diff --git a/.dockerignore b/.dockerignore index 6a47c2536..d60c2cb95 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,10 @@ backend/node_modules -frontend/node_modules \ No newline at end of file +frontend/node_modules +backend/frontend-build +**/node_modules +**/.next +.dockerignore +.git +README.md +.dockerignore +**/Dockerfile diff --git a/.env.example b/.env.example index e98401462..6b8639a74 100644 --- a/.env.example +++ b/.env.example @@ -1,24 +1,12 @@ # Keys # Required key for platform encryption/decryption ops -# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NOT BE USED FOR PRODUCTION +# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 # JWT # Required secrets to sign JWT tokens -JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a -JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff -JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f -JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200 -JWT_SERVICE_TOKEN_SECRET=f32f716d70a42c5703f4656015e76200 -JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201 - -# JWT lifetime -# Optional lifetimes for JWT tokens expressed in seconds or a string -# describing a time span (e.g. 60, "2 days", "10h", "7d") -JWT_AUTH_LIFETIME= -JWT_REFRESH_LIFETIME= -JWT_SIGNUP_LIFETIME= -JWT_PROVIDER_AUTH_LIFETIME= +# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION +AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE= # MongoDB # Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref @@ -68,5 +56,12 @@ SENTRY_DSN= POSTHOG_HOST= POSTHOG_PROJECT_API_KEY= -CLIENT_ID_GOOGLE= -CLIENT_SECRET_GOOGLE= +# SSO-specific variables +CLIENT_ID_GOOGLE_LOGIN= +CLIENT_SECRET_GOOGLE_LOGIN= + +CLIENT_ID_GITHUB_LOGIN= +CLIENT_SECRET_GITHUB_LOGIN= + +CLIENT_ID_GITLAB_LOGIN= +CLIENT_SECRET_GITLAB_LOGIN= diff --git a/.github/resources/docker-compose.be-test.yml b/.github/resources/docker-compose.be-test.yml index c256649c1..373c88dc0 100644 --- a/.github/resources/docker-compose.be-test.yml +++ b/.github/resources/docker-compose.be-test.yml @@ -6,7 +6,7 @@ services: restart: unless-stopped depends_on: - mongo - image: infisical/backend:test + image: infisical/infisical:test command: npm run start environment: - NODE_ENV=production diff --git a/.github/values.yaml b/.github/values.yaml index 93aa94767..6c9367736 100644 --- a/.github/values.yaml +++ b/.github/values.yaml @@ -1,29 +1,3 @@ -# secretScanningGitApp: -# enabled: false -# deploymentAnnotations: -# secrets.infisical.com/auto-reload: "true" -# image: -# repository: infisical/staging_deployment_secret-scanning-git-app - -frontend: - enabled: true - name: frontend - podAnnotations: {} - deploymentAnnotations: - secrets.infisical.com/auto-reload: "true" - replicaCount: 2 - image: - repository: infisical/staging_deployment_frontend - tag: "latest" - pullPolicy: Always - kubeSecretRef: managed-secret-frontend - service: - annotations: {} - type: ClusterIP - nodePort: "" - -frontendEnvironmentVariables: null - backend: enabled: true name: backend @@ -32,7 +6,7 @@ backend: secrets.infisical.com/auto-reload: "true" replicaCount: 2 image: - repository: infisical/staging_deployment_backend + repository: infisical/staging_infisical tag: "latest" pullPolicy: Always kubeSecretRef: managed-backend-secret @@ -63,14 +37,8 @@ ingress: enabled: true # annotations: # kubernetes.io/ingress.class: "nginx" - # cert-manager.io/issuer: letsencrypt-nginx + # cert-manager.io/issuer: letsencrypt-nginx hostName: gamma.infisical.com ## <- Replace with your own domain - frontend: - path: / - pathType: Prefix - backend: - path: /api - pathType: Prefix tls: [] # - secretName: letsencrypt-nginx diff --git a/.github/workflows/build-docker-image-to-prod.yml b/.github/workflows/build-docker-image-to-prod.yml index bea66e3fc..04e40f1e9 100644 --- a/.github/workflows/build-docker-image-to-prod.yml +++ b/.github/workflows/build-docker-image-to-prod.yml @@ -39,7 +39,7 @@ jobs: token: ${{ secrets.DEPOT_PROJECT_TOKEN }} load: true context: backend - tags: infisical/backend:test + tags: infisical/infisical:test - name: โป Spawn backend container and dependencies run: | docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull diff --git a/.github/workflows/build-staging-img.yml b/.github/workflows/build-staging-img.yml index f4233c713..6e094871e 100644 --- a/.github/workflows/build-staging-img.yml +++ b/.github/workflows/build-staging-img.yml @@ -2,7 +2,7 @@ name: Build, Publish and Deploy to Gamma on: [workflow_dispatch] jobs: - backend-image: + infisical-image: name: Build backend image runs-on: ubuntu-latest steps: @@ -32,8 +32,9 @@ jobs: project: 64mmf0n610 token: ${{ secrets.DEPOT_PROJECT_TOKEN }} load: true - context: backend - tags: infisical/backend:test + context: . + file: Dockerfile.standalone-infisical + tags: infisical/infisical:test - name: โป Spawn backend container and dependencies run: | docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull @@ -49,68 +50,20 @@ jobs: project: 64mmf0n610 token: ${{ secrets.DEPOT_PROJECT_TOKEN }} push: true - context: backend + context: . + file: Dockerfile.standalone-infisical tags: | - infisical/staging_deployment_backend:${{ steps.commit.outputs.short }} - infisical/staging_deployment_backend:latest + 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 }} - frontend-image: - name: Build frontend image - runs-on: ubuntu-latest - steps: - - name: โ˜๏ธ Checkout source - uses: actions/checkout@v3 - - 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 frontend and export to Docker - uses: depot/build-push-action@v1 - with: - load: true - token: ${{ secrets.DEPOT_PROJECT_TOKEN }} - project: 64mmf0n610 - context: frontend - tags: infisical/staging_deployment_frontend:test - build-args: | - POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} - NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} - - name: โป Spawn frontend container - run: | - docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test - - name: ๐Ÿงช Test frontend image - run: | - ./.github/resources/healthcheck.sh infisical-frontend-test - - name: โป Shut down frontend container - run: | - docker stop infisical-frontend-test - - name: ๐Ÿ—๏ธ Build frontend and push - uses: depot/build-push-action@v1 - with: - project: 64mmf0n610 - push: true - token: ${{ secrets.DEPOT_PROJECT_TOKEN }} - context: frontend - tags: | - infisical/staging_deployment_frontend:${{ steps.commit.outputs.short }} - infisical/staging_deployment_frontend:latest - platforms: linux/amd64,linux/arm64 - build-args: | - POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} - NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} gamma-deployment: name: Deploy to gamma runs-on: ubuntu-latest - needs: [frontend-image, backend-image] + needs: [infisical-image] steps: - name: โ˜๏ธ Checkout source uses: actions/checkout@v3 diff --git a/.github/workflows/release-standalone-docker-img.yml b/.github/workflows/release-standalone-docker-img.yml index 5d91bd59c..fd1d59c59 100644 --- a/.github/workflows/release-standalone-docker-img.yml +++ b/.github/workflows/release-standalone-docker-img.yml @@ -73,3 +73,6 @@ jobs: infisical/infisical:${{ steps.extract_version.outputs.version }} platforms: linux/amd64,linux/arm64 file: Dockerfile.standalone-infisical + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} + INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} diff --git a/.gitignore b/.gitignore index da6eb5142..24dc274ba 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,7 @@ reports junit.xml # next.js -/.next/ +.next/ /out/ # production @@ -59,4 +59,6 @@ yarn-error.log* .infisical.json # Editor specific -.vscode/* \ No newline at end of file +.vscode/* + +frontend-build \ No newline at end of file diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 8770bb234..56d6735b0 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -1,7 +1,13 @@ ARG POSTHOG_HOST=https://app.posthog.com ARG POSTHOG_API_KEY=posthog-api-key +ARG INTERCOM_ID=intercom-id -FROM node:16-alpine AS frontend-dependencies +FROM node:16-alpine AS base + +FROM base AS frontend-dependencies + +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat WORKDIR /app @@ -11,7 +17,7 @@ COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./ RUN npm ci --only-production --ignore-scripts # Rebuild the source code only when needed -FROM node:16-alpine AS frontend-builder +FROM base AS frontend-builder WORKDIR /app # Copy dependencies @@ -27,41 +33,38 @@ ARG POSTHOG_API_KEY ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY ARG INTERCOM_ID ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID +ARG INFISICAL_PLATFORM_VERSION +ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION # Build RUN npm run build # Production image -FROM node:16-alpine AS frontend-runner +FROM base AS frontend-runner WORKDIR /app RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs +RUN adduser --system --uid 1001 non-root-user -RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images +RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images VOLUME /app/.next/cache/images -ARG POSTHOG_API_KEY -ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ - BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY -ARG INTERCOM_ID -ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ - BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID - -COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts +COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts COPY --from=frontend-builder /app/public ./public -RUN chown nextjs:nodejs ./public/data -COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/static ./.next/static +RUN chown non-root-user:nodejs ./public/data +COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./ +COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static -USER nextjs +USER non-root-user ENV NEXT_TELEMETRY_DISABLED 1 ## ## BACKEND ## -FROM node:16-alpine AS backend-build +FROM base AS backend-build +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 non-root-user WORKDIR /app @@ -69,10 +72,11 @@ COPY backend/package*.json ./ RUN npm ci --only-production COPY /backend . +COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh RUN npm run build # Production stage -FROM node:16-alpine AS backend-runner +FROM base AS backend-runner WORKDIR /app @@ -81,27 +85,44 @@ RUN npm ci --only-production COPY --from=backend-build /app . +RUN mkdir frontend-build + # Production stage -FROM node:16-alpine AS production +FROM base AS production +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 non-root-user + +## set pre baked keys +ARG POSTHOG_API_KEY +ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ + BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY +ARG INTERCOM_ID=intercom-id +ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ + BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID WORKDIR / -# Install PM2 -RUN npm install -g pm2 -# Copy ecosystem.config.js -COPY ecosystem.config.js . - -RUN apk add --no-cache nginx - -COPY nginx/default-stand-alone-docker.conf /etc/nginx/nginx.conf - COPY --from=backend-runner /app /backend -COPY --from=frontend-runner /app/ /app/ +COPY --from=frontend-runner /app ./backend/frontend-build -EXPOSE 80 +ENV PORT 8080 ENV HTTPS_ENABLED false +ENV NODE_ENV production +ENV STANDALONE_BUILD true + +WORKDIR /backend + +ENV TELEMETRY_ENABLED true + +HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ + CMD node healthcheck.js + +EXPOSE 8080 + +USER non-root-user + +CMD ["./standalone-entrypoint.sh"] -CMD ["pm2-runtime", "start", "ecosystem.config.js"] diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 5c3e2f819..402fd5874 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -17,17 +17,13 @@ export const getRootEncryptionKey = async () => { } export const getInviteOnlySignup = async () => (await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true" export const getSaltRounds = async () => parseInt((await client.getSecret("SALT_ROUNDS")).secretValue) || 10; +export const getAuthSecret = async () => (await client.getSecret("JWT_AUTH_SECRET")).secretValue ?? (await client.getSecret("AUTH_SECRET")).secretValue; export const getJwtAuthLifetime = async () => (await client.getSecret("JWT_AUTH_LIFETIME")).secretValue || "10d"; -export const getJwtAuthSecret = async () => (await client.getSecret("JWT_AUTH_SECRET")).secretValue; export const getJwtMfaLifetime = async () => (await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m"; -export const getJwtMfaSecret = async () => (await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m"; export const getJwtRefreshLifetime = async () => (await client.getSecret("JWT_REFRESH_LIFETIME")).secretValue || "90d"; -export const getJwtRefreshSecret = async () => (await client.getSecret("JWT_REFRESH_SECRET")).secretValue; -export const getJwtServiceSecret = async () => (await client.getSecret("JWT_SERVICE_SECRET")).secretValue; +export const getJwtServiceSecret = async () => (await client.getSecret("JWT_SERVICE_SECRET")).secretValue; // TODO: deprecate (related to ST V1) export const getJwtSignupLifetime = async () => (await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m"; -export const getJwtProviderAuthSecret = async () => (await client.getSecret("JWT_PROVIDER_AUTH_SECRET")).secretValue; export const getJwtProviderAuthLifetime = async () => (await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m"; -export const getJwtSignupSecret = async () => (await client.getSecret("JWT_SIGNUP_SECRET")).secretValue; export const getJwtServiceTokenSecret = async () => (await client.getSecret("JWT_SERVICE_TOKEN_SECRET")).secretValue; export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue; export const getNodeEnv = async () => (await client.getSecret("NODE_ENV")).secretValue || "production"; diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 6f6411541..e426eef5a 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -6,15 +6,18 @@ const jsrp = require("jsrp"); import { LoginSRPDetail, TokenVersion, User } from "../../models"; import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth"; import { checkUserDevice } from "../../helpers/user"; -import { ACTION_LOGIN, ACTION_LOGOUT } from "../../variables"; +import { + ACTION_LOGIN, + ACTION_LOGOUT, + AuthTokenType +} from "../../variables"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { EELogService } from "../../ee/services"; import { getUserAgentType } from "../../utils/posthog"; import { + getAuthSecret, getHttpsEnabled, - getJwtAuthLifetime, - getJwtAuthSecret, - getJwtRefreshSecret + getJwtAuthLifetime } from "../../config"; import { ActorType } from "../../ee/models"; import { validateRequest } from "../../helpers/validation"; @@ -238,6 +241,7 @@ export const checkAuth = async (req: Request, res: Response) => { * @returns */ export const getNewToken = async (req: Request, res: Response) => { + const refreshToken = req.cookies.jid; if (!refreshToken) @@ -245,7 +249,9 @@ export const getNewToken = async (req: Request, res: Response) => { message: "Failed to find refresh token in request cookies" }); - const decodedToken = jwt.verify(refreshToken, await getJwtRefreshSecret()); + const decodedToken = jwt.verify(refreshToken, await getAuthSecret()); + + if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) throw UnauthorizedRequestError(); const user = await User.findOne({ _id: decodedToken.userId @@ -268,12 +274,13 @@ export const getNewToken = async (req: Request, res: Response) => { const token = createToken({ payload: { + authTokenType: AuthTokenType.ACCESS_TOKEN, userId: decodedToken.userId, tokenVersionId: tokenVersion._id.toString(), accessVersion: tokenVersion.refreshVersion }, expiresIn: await getJwtAuthLifetime(), - secret: await getJwtAuthSecret() + secret: await getAuthSecret() }); return res.status(200).send({ diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index 57ebb79aa..eeba96783 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -8,11 +8,11 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; import { sendMail } from "../../helpers/nodemailer"; import { TokenService } from "../../services"; import { EELicenseService } from "../../ee/services"; -import { ACCEPTED, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables"; +import { ACCEPTED, AuthTokenType, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables"; import * as reqValidator from "../../validation/membershipOrg"; import { + getAuthSecret, getJwtSignupLifetime, - getJwtSignupSecret, getSiteURL, getSmtpConfigured } from "../../config"; @@ -272,10 +272,11 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { // generate temporary signup token const token = createToken({ payload: { + authTokenType: AuthTokenType.SIGNUP_TOKEN, userId: user._id.toString() }, expiresIn: await getJwtSignupLifetime(), - secret: await getJwtSignupSecret() + secret: await getAuthSecret() }); return res.status(200).send({ diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index 65447bcf8..d0b59f317 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -5,12 +5,12 @@ import * as bigintConversion from "bigint-conversion"; import { BackupPrivateKey, LoginSRPDetail, User } from "../../models"; import { clearTokens, createToken, sendMail } from "../../helpers"; import { TokenService } from "../../services"; -import { TOKEN_EMAIL_PASSWORD_RESET } from "../../variables"; +import { AuthTokenType, TOKEN_EMAIL_PASSWORD_RESET } from "../../variables"; import { BadRequestError } from "../../utils/errors"; import { + getAuthSecret, getHttpsEnabled, getJwtSignupLifetime, - getJwtSignupSecret, getSiteURL } from "../../config"; import { ActorType } from "../../ee/models"; @@ -88,10 +88,11 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => { // generate temporary password-reset token const token = createToken({ payload: { + authTokenType: AuthTokenType.SIGNUP_TOKEN, userId: user._id.toString() }, expiresIn: await getJwtSignupLifetime(), - secret: await getJwtSignupSecret() + secret: await getAuthSecret() }); return res.status(200).send({ diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index 68a4e07f2..4dceb2783 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -4,14 +4,15 @@ import { checkEmailVerification, sendEmailVerification } from "../../helpers/sig import { createToken } from "../../helpers/auth"; import { BadRequestError } from "../../utils/errors"; import { + getAuthSecret, getInviteOnlySignup, getJwtSignupLifetime, - getJwtSignupSecret, getSmtpConfigured } from "../../config"; import { validateUserEmail } from "../../validation"; import { validateRequest } from "../../helpers/validation"; import * as reqValidator from "../../validation/auth"; +import { AuthTokenType } from "../../variables"; /** * Signup step 1: Initialize account for user under email [email] and send a verification code @@ -95,10 +96,11 @@ export const verifyEmailSignup = async (req: Request, res: Response) => { // generate temporary signup token const token = createToken({ payload: { + authTokenType: AuthTokenType.SIGNUP_TOKEN, userId: user._id.toString() }, expiresIn: await getJwtSignupLifetime(), - secret: await getJwtSignupSecret() + secret: await getAuthSecret() }); return res.status(200).send({ diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index d75206c64..46c288ea0 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -10,9 +10,9 @@ import { sendMail } from "../../helpers/nodemailer"; import { TokenService } from "../../services"; import { EELogService } from "../../ee/services"; import { BadRequestError, InternalServerError } from "../../utils/errors"; -import { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables"; +import { ACTION_LOGIN, AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables"; import { getUserAgentType } from "../../utils/posthog"; // TODO: move this -import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config"; +import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config"; import { validateRequest } from "../../helpers/validation"; import * as reqValidator from "../../validation/auth"; @@ -109,10 +109,11 @@ export const login2 = async (req: Request, res: Response) => { // generate temporary MFA token const token = createToken({ payload: { + authTokenType: AuthTokenType.MFA_TOKEN, userId: user._id.toString() }, expiresIn: await getJwtMfaLifetime(), - secret: await getJwtMfaSecret() + secret: await getAuthSecret() }); const code = await TokenService.createToken({ diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index 1b228a39b..9cd23cf87 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -10,9 +10,9 @@ import { sendMail } from "../../helpers/nodemailer"; import { TokenService } from "../../services"; import { EELogService } from "../../ee/services"; import { BadRequestError, InternalServerError } from "../../utils/errors"; -import { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables"; +import { ACTION_LOGIN, AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables"; import { getUserAgentType } from "../../utils/posthog"; // TODO: move this -import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config"; +import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config"; import { AuthMethod } from "../../models/user"; import { validateRequest } from "../../helpers/validation"; import * as reqValidator from "../../validation/auth"; @@ -134,10 +134,11 @@ export const login2 = async (req: Request, res: Response) => { // generate temporary MFA token const token = createToken({ payload: { + authTokenType: AuthTokenType.MFA_TOKEN, userId: user._id.toString() }, expiresIn: await getJwtMfaLifetime(), - secret: await getJwtMfaSecret() + secret: await getAuthSecret() }); const code = await TokenService.createToken({ diff --git a/backend/src/controllers/v3/index.ts b/backend/src/controllers/v3/index.ts index e52b1a0ea..b52e0aa41 100644 --- a/backend/src/controllers/v3/index.ts +++ b/backend/src/controllers/v3/index.ts @@ -1,9 +1,11 @@ +import * as usersController from "./usersController"; import * as secretsController from "./secretsController"; import * as workspacesController from "./workspacesController"; import * as authController from "./authController"; import * as signupController from "./signupController"; export { + usersController, authController, secretsController, signupController, diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 4db0dca94..2e0c8514a 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -476,7 +476,7 @@ export const getSecrets = async (req: Request, res: Response) => { if (folderId && folderId !== "root") { const folder = await Folder.findOne({ workspace: workspaceId, environment }); - if (!folder) throw BadRequestError({ message: "Folder not found" }); + if (!folder) return res.send({ secrets: [] }); secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath; } @@ -673,6 +673,7 @@ export const updateSecretByName = async (req: Request, res: Response) => { secretValueCiphertext, secretValueTag, secretValueIV, + secretId, type, environment, secretPath, @@ -741,6 +742,7 @@ export const updateSecretByName = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, type, + secretId, authData: req.authData, newSecretName, secretValueCiphertext, @@ -961,6 +963,14 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => { authData: req.authData }); + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath + }) + }); + return res.status(200).send({ secrets: deletedSecrets }); diff --git a/backend/src/controllers/v3/signupController.ts b/backend/src/controllers/v3/signupController.ts index 79d661b58..d16fbe9ca 100644 --- a/backend/src/controllers/v3/signupController.ts +++ b/backend/src/controllers/v3/signupController.ts @@ -5,10 +5,10 @@ import { MembershipOrg, User } from "../../models"; import { completeAccount } from "../../helpers/user"; import { initializeDefaultOrg } from "../../helpers/signup"; import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth"; -import { ACCEPTED, INVITED } from "../../variables"; +import { ACCEPTED, AuthTokenType, INVITED } from "../../variables"; import { standardRequest } from "../../config/request"; -import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config"; -import { BadRequestError } from "../../utils/errors"; +import { getAuthSecret, getHttpsEnabled, getLoopsApiKey } from "../../config"; +import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { TelemetryService } from "../../services"; import { AuthMethod } from "../../models"; import { validateRequest } from "../../helpers/validation"; @@ -78,12 +78,11 @@ export const completeAccountSignup = async (req: Request, res: Response) => { } const decodedToken = ( - jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret()) + jwt.verify(AUTH_TOKEN_VALUE, await getAuthSecret()) ); - - if (decodedToken.userId !== user.id) { - throw BadRequestError(); - } + + if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw UnauthorizedRequestError(); + if (decodedToken.userId !== user.id) throw UnauthorizedRequestError(); } // complete setting up user's account diff --git a/backend/src/controllers/v3/usersController.ts b/backend/src/controllers/v3/usersController.ts new file mode 100644 index 000000000..e94173540 --- /dev/null +++ b/backend/src/controllers/v3/usersController.ts @@ -0,0 +1,18 @@ +import { Request, Response } from "express"; +import { APIKeyDataV2 } from "../../models"; + +/** + * Return API keys belonging to current user. + * @param req + * @param res + * @returns + */ +export const getMyAPIKeys = async (req: Request, res: Response) => { + const apiKeyData = await APIKeyDataV2.find({ + user: req.user._id + }); + + return res.status(200).send({ + apiKeyData + }); +} \ No newline at end of file diff --git a/backend/src/ee/controllers/v3/apiKeyDataController.ts b/backend/src/ee/controllers/v3/apiKeyDataController.ts new file mode 100644 index 000000000..1fe7d6d3b --- /dev/null +++ b/backend/src/ee/controllers/v3/apiKeyDataController.ts @@ -0,0 +1,101 @@ +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { APIKeyDataV2 } from "../../../models/apiKeyDataV2"; +import { validateRequest } from "../../../helpers/validation"; +import { BadRequestError } from "../../../utils/errors"; +import * as reqValidator from "../../../validation"; +import { createToken } from "../../../helpers"; +import { AuthTokenType } from "../../../variables"; +import { getAuthSecret } from "../../../config"; + +/** + * Create API key data v2 + * @param req + * @param res + */ +export const createAPIKeyData = async (req: Request, res: Response) => { + const { + body: { + name + } + } = await validateRequest(reqValidator.CreateAPIKeyV3, req); + + const apiKeyData = await new APIKeyDataV2({ + name, + user: req.user._id, + usageCount: 0, + }).save(); + + const apiKey = createToken({ + payload: { + authTokenType: AuthTokenType.API_KEY, + apiKeyDataId: apiKeyData._id.toString(), + userId: req.user._id.toString() + }, + secret: await getAuthSecret() + }); + + return res.status(200).send({ + apiKeyData, + apiKey + }); +} + +/** + * Update API key data v2 with id [apiKeyDataId] + * @param req + * @param res + */ + export const updateAPIKeyData = async (req: Request, res: Response) => { + const { + params: { apiKeyDataId }, + body: { + name, + } + } = await validateRequest(reqValidator.UpdateAPIKeyV3, req); + + const apiKeyData = await APIKeyDataV2.findOneAndUpdate( + { + _id: new Types.ObjectId(apiKeyDataId), + user: req.user._id + }, + { + name + }, + { + new: true + } + ); + + if (!apiKeyData) throw BadRequestError({ + message: "Failed to update API key" + }); + + return res.status(200).send({ + apiKeyData + }); +} + +/** + * Delete API key data v2 with id [apiKeyDataId] + * @param req + * @param res + */ + export const deleteAPIKeyData = async (req: Request, res: Response) => { + const { + params: { apiKeyDataId } + } = await validateRequest(reqValidator.DeleteAPIKeyV3, req); + + const apiKeyData = await APIKeyDataV2.findOneAndDelete({ + _id: new Types.ObjectId(apiKeyDataId), + user: req.user._id + }); + + if (!apiKeyData) throw BadRequestError({ + message: "Failed to delete API key" + }); + + return res.status(200).send({ + apiKeyData + }); +} \ No newline at end of file diff --git a/backend/src/ee/controllers/v3/index.ts b/backend/src/ee/controllers/v3/index.ts index af6f3d306..454e0c446 100644 --- a/backend/src/ee/controllers/v3/index.ts +++ b/backend/src/ee/controllers/v3/index.ts @@ -1,5 +1,7 @@ import * as serviceTokenDataController from "./serviceTokenDataController"; +import * as apiKeyDataController from "./apiKeyDataController"; export { - serviceTokenDataController + serviceTokenDataController, + apiKeyDataController } \ No newline at end of file diff --git a/backend/src/ee/controllers/v3/serviceTokenDataController.ts b/backend/src/ee/controllers/v3/serviceTokenDataController.ts index 6b6507cd0..d233cb0d9 100644 --- a/backend/src/ee/controllers/v3/serviceTokenDataController.ts +++ b/backend/src/ee/controllers/v3/serviceTokenDataController.ts @@ -30,7 +30,7 @@ import { EEAuditLogService, EELicenseService } from "../../services"; import { getJwtServiceTokenSecret } from "../../../config"; /** - * Return project key for service token + * Return project key for service token V3 * @param req * @param res */ @@ -57,7 +57,7 @@ export const getServiceTokenDataKey = async (req: Request, res: Response) => { } /** - * Create service token data + * Create service token data V3 * @param req * @param res * @returns @@ -165,7 +165,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { } /** - * Update service token data with id [serviceTokenDataId] + * Update service token V3 data with id [serviceTokenDataId] * @param req * @param res * @returns diff --git a/backend/src/ee/routes/v1/sso.ts b/backend/src/ee/routes/v1/sso.ts index baa75d505..24f0d36a1 100644 --- a/backend/src/ee/routes/v1/sso.ts +++ b/backend/src/ee/routes/v1/sso.ts @@ -13,7 +13,10 @@ router.get( const options = { failureRedirect: "/", additionalParams: { - RelayState: req.query.callback_port ?? "" + RelayState: JSON.stringify({ + spInitiated: true, + callbackPort: req.query.callback_port ?? "" + }) }, }; passport.authenticate("saml", options)(req, res, next); diff --git a/backend/src/ee/routes/v3/apiKeyData.ts b/backend/src/ee/routes/v3/apiKeyData.ts new file mode 100644 index 000000000..6d069a719 --- /dev/null +++ b/backend/src/ee/routes/v3/apiKeyData.ts @@ -0,0 +1,31 @@ +import express from "express"; +const router = express.Router(); +import { requireAuth } from "../../../middleware"; +import { AuthMode } from "../../../variables"; +import { apiKeyDataController } from "../../controllers/v3"; + +router.post( + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + apiKeyDataController.createAPIKeyData +); + +router.patch( + "/:apiKeyDataId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + apiKeyDataController.updateAPIKeyData +); + +router.delete( + "/:apiKeyDataId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + apiKeyDataController.deleteAPIKeyData +); + +export default router; \ No newline at end of file diff --git a/backend/src/ee/routes/v3/index.ts b/backend/src/ee/routes/v3/index.ts index 7f75a2755..dd8c13427 100644 --- a/backend/src/ee/routes/v3/index.ts +++ b/backend/src/ee/routes/v3/index.ts @@ -1,5 +1,7 @@ import serviceTokenData from "./serviceTokenData"; +import apiKeyData from "./apiKeyData"; export { - serviceTokenData + serviceTokenData, + apiKeyData } \ No newline at end of file diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 31e584fe5..a8b13fd82 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -4,6 +4,7 @@ import jwt from "jsonwebtoken"; import bcrypt from "bcrypt"; import { APIKeyData, + APIKeyDataV2, ITokenVersion, IUser, ServiceTokenData, @@ -19,15 +20,14 @@ import { UnauthorizedRequestError, } from "../utils/errors"; import { + getAuthSecret, getJwtAuthLifetime, - getJwtAuthSecret, - getJwtProviderAuthSecret, getJwtRefreshLifetime, - getJwtRefreshSecret, getJwtServiceTokenSecret } from "../config"; import { - AuthMode + AuthMode, + AuthTokenType } from "../variables"; import { ServiceTokenAuthData, @@ -51,8 +51,6 @@ export const validateAuthMode = ({ acceptedAuthModes: AuthMode[] }) => { - // TODO: update this to accept service token v3 - const apiKey = headers["x-api-key"]; const authHeader = headers["authorization"]; @@ -108,6 +106,7 @@ export const validateAuthMode = ({ /** * Return user payload corresponding to JWT token [authTokenValue] + * that is either for browser / CLI or API Key * @param {Object} obj * @param {String} obj.authTokenValue - JWT token value * @returns {User} user - user corresponding to JWT token @@ -120,9 +119,45 @@ export const getAuthUserPayload = async ({ authTokenValue: string; }): Promise => { const decodedToken = ( - jwt.verify(authTokenValue, await getJwtAuthSecret()) + jwt.verify(authTokenValue, await getAuthSecret()) ); + if ( + decodedToken.authTokenType !== AuthTokenType.ACCESS_TOKEN && + decodedToken.authTokenType !== AuthTokenType.API_KEY + ) { + throw UnauthorizedRequestError(); + } + + if (decodedToken.authTokenType === AuthTokenType.ACCESS_TOKEN) { + const tokenVersion = await TokenVersion.findOneAndUpdate({ + _id: new Types.ObjectId(decodedToken.tokenVersionId), + user: decodedToken.userId + }, { + lastUsed: new Date(), + }); + + if (!tokenVersion) throw UnauthorizedRequestError(); + + if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError(); + } else if (decodedToken.authTokenType === AuthTokenType.API_KEY) { + const apiKeyData = await APIKeyDataV2.findOneAndUpdate( + { + _id: new Types.ObjectId(decodedToken.apiKeyDataId), + user: new Types.ObjectId(decodedToken.userId) + }, + { + lastUsed: new Date(), + $inc: { usageCount: 1 } + }, + { + new: true + } + ); + + if (!apiKeyData) throw UnauthorizedRequestError(); + } + const user = await User.findOne({ _id: new Types.ObjectId(decodedToken.userId), }).select("+publicKey +accessVersion"); @@ -131,21 +166,6 @@ export const getAuthUserPayload = async ({ if (!user?.publicKey) throw UnauthorizedRequestError({ message: "Failed to authenticate user with partially set up account" }); - const tokenVersion = await TokenVersion.findOneAndUpdate({ - _id: new Types.ObjectId(decodedToken.tokenVersionId), - user: user._id, - }, { - lastUsed: new Date(), - }); - - if (!tokenVersion) throw UnauthorizedRequestError({ - message: "Failed to validate access token", - }); - - if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError({ - message: "Failed to validate access token", - }); - return { actor: { type: ActorType.USER, @@ -159,11 +179,6 @@ export const getAuthUserPayload = async ({ userAgent: req.headers["user-agent"] ?? "", userAgentType: getUserAgentType(req.headers["user-agent"]) } - - // return ({ - // user, - // tokenVersionId: tokenVersion._id, // what to do with this? // move this out - // }); } /** @@ -404,22 +419,24 @@ export const issueAuthTokens = async ({ // issue tokens const token = createToken({ payload: { + authTokenType: AuthTokenType.ACCESS_TOKEN, userId, tokenVersionId: tokenVersion._id.toString(), accessVersion: tokenVersion.accessVersion, }, expiresIn: await getJwtAuthLifetime(), - secret: await getJwtAuthSecret(), + secret: await getAuthSecret(), }); const refreshToken = createToken({ payload: { + authTokenType: AuthTokenType.REFRESH_TOKEN, userId, tokenVersionId: tokenVersion._id.toString(), refreshVersion: tokenVersion.refreshVersion, }, expiresIn: await getJwtRefreshLifetime(), - secret: await getJwtRefreshSecret(), + secret: await getAuthSecret(), }); return { @@ -451,7 +468,7 @@ export const clearTokens = async (tokenVersionId: Types.ObjectId): Promise * bearer/auth, refresh, and temporary signup tokens * @param {Object} obj * @param {Object} obj.payload - payload of (JWT) token - * @param {String} obj.secret - (JWT) secret such as [JWT_AUTH_SECRET] + * @param {String} obj.secret - (JWT) secret such as [AUTH_SECRET] * @param {String} obj.expiresIn - string describing time span such as '10h' or '7d' */ export const createToken = ({ @@ -479,13 +496,16 @@ export const validateProviderAuthToken = async ({ email: string; providerAuthToken?: string; }) => { + if (!providerAuthToken) { throw new Error("Invalid authentication request."); } const decodedToken = ( - jwt.verify(providerAuthToken, await getJwtProviderAuthSecret()) + jwt.verify(providerAuthToken, await getAuthSecret()) ); + + if (decodedToken.authTokenType !== AuthTokenType.PROVIDER_TOKEN) throw UnauthorizedRequestError(); if (decodedToken.email !== email) { throw new Error("Invalid authentication credentials.") diff --git a/backend/src/helpers/database.ts b/backend/src/helpers/database.ts index 4780be346..611e941f5 100644 --- a/backend/src/helpers/database.ts +++ b/backend/src/helpers/database.ts @@ -14,7 +14,7 @@ export const initDatabaseHelper = async ({ }) => { try { await mongoose.connect(mongoURL); - + // allow empty strings to pass the required validator mongoose.Schema.Types.String.checkRequired(v => typeof v === "string"); @@ -31,14 +31,10 @@ export const initDatabaseHelper = async ({ * Close database conection */ export const closeDatabaseHelper = async () => { - return Promise.all([ - new Promise((resolve) => { - if (mongoose.connection && mongoose.connection.readyState == 1) { - mongoose.connection.close() - .then(() => resolve("Database connection closed")); - } else { - resolve("Database connection already closed"); - } - }), - ]); -} \ No newline at end of file + if (mongoose.connection && mongoose.connection.readyState === 1) { + await mongoose.connection.close(); + return "Database connection closed"; + } else { + return "Database connection already closed"; + } +}; \ No newline at end of file diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index ea6f4a254..82a98d1e9 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -790,6 +790,7 @@ export const getSecretHelper = async ({ export const updateSecretHelper = async ({ secretName, workspaceId, + secretId, environment, type, authData, @@ -812,11 +813,20 @@ export const updateSecretHelper = async ({ workspaceId: new Types.ObjectId(workspaceId) }); - const oldSecretBlindIndex = await generateSecretBlindIndexWithSaltHelper({ + let oldSecretBlindIndex = await generateSecretBlindIndexWithSaltHelper({ secretName, salt }); + if (secretId) { + const secret = await Secret.findOne({ + workspace: workspaceId, + environment, + _id: secretId + }).select("secretBlindIndex"); + if (secret && secret.secretBlindIndex) oldSecretBlindIndex = secret.secretBlindIndex; + } + let secret: ISecret | null = null; const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); @@ -891,6 +901,9 @@ export const updateSecretHelper = async ({ skipMultilineEncoding, secretBlindIndex: newSecretNameBlindIndex, $inc: { version: 1 } + }, + { + new: true } ); } @@ -1748,6 +1761,22 @@ export const deleteSecretBatchHelper = async ({ secretIds: deletedSecrets.map((secret) => secret._id) }); + const action = await EELogService.createAction({ + name: ACTION_DELETE_SECRETS, + ...getAuthDataPayloadIdObj(authData), + workspaceId, + secretIds: deletedSecrets.map((secret) => secret._id) + }); + + action && + (await EELogService.createLog({ + ...getAuthDataPayloadIdObj(authData), + workspaceId, + actions: [action], + channel: authData.userAgentType, + ipAddress: authData.ipAddress + })); + await EEAuditLogService.createAuditLog( authData, { diff --git a/backend/src/index.ts b/backend/src/index.ts index 08b159d42..aa4ac2cd5 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -29,6 +29,7 @@ import { secretApprovalRequest as v1SecretApprovalRequest, secretScanning as v1SecretScanningRouter } from "./ee/routes/v1"; +import { apiKeyData as v3apiKeyDataRouter } from "./ee/routes/v3"; import { serviceTokenData as v3ServiceTokenDataRouter } from "./ee/routes/v3"; import { auth as v1AuthRouter, @@ -68,6 +69,7 @@ import { auth as v3AuthRouter, secrets as v3SecretsRouter, signup as v3SignupRouter, + users as v3UsersRouter, workspaces as v3WorkspacesRouter } from "./routes/v3"; import { healthCheck } from "./routes/status"; @@ -81,12 +83,15 @@ import { getSecretScanningPrivateKey, getSecretScanningWebhookProxy, getSecretScanningWebhookSecret, - getSiteURL + getSiteURL, } from "./config"; import { setup } from "./utils/setup"; import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices"; import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent"; const SmeeClient = require("smee-client"); // eslint-disable-line +import path from "path"; + +let handler: null | any = null; const main = async () => { await setup(); @@ -147,6 +152,27 @@ const main = async () => { next(); }); + if ((await getNodeEnv()) === "production" && process.env.STANDALONE_BUILD === "true") { + const nextJsBuildPath = path.join(__dirname, "../frontend-build"); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-var-requires + const conf = require("../frontend-build/.next/required-server-files.json").config; + const NextServer = + // eslint-disable-next-line @typescript-eslint/no-var-requires + require("../frontend-build/node_modules/next/dist/server/next-server").default; + const nextApp = new NextServer({ + dev: false, + dir: nextJsBuildPath, + port: await getPort(), + conf, + hostname: "local", + customServer: false + }); + + handler = nextApp.getRequestHandler(); + } + // (EE) routes app.use("/api/v1/secret", eeSecretRouter); app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter); @@ -156,7 +182,8 @@ const main = async () => { app.use("/api/v1/organizations", eeOrganizationsRouter); app.use("/api/v1/sso", eeSSORouter); app.use("/api/v1/cloud-products", eeCloudProductsRouter); - app.use("/api/v3/service-token", v3ServiceTokenDataRouter); + app.use("/api/v3/api-key", v3apiKeyDataRouter); // new + app.use("/api/v3/service-token", v3ServiceTokenDataRouter); // new // v1 routes app.use("/api/v1/signup", v1SignupRouter); @@ -202,6 +229,7 @@ const main = async () => { app.use("/api/v3/secrets", v3SecretsRouter); app.use("/api/v3/workspaces", v3WorkspacesRouter); app.use("/api/v3/signup", v3SignupRouter); + app.use("/api/v3/users", v3UsersRouter); // api docs app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile)); @@ -209,6 +237,12 @@ const main = async () => { // server status app.use("/api", healthCheck); + if (handler) { + app.all("*", (req, res) => { + return handler(req, res); + }); + } + //* Handle unrouted requests and respond with proper error message as well as status code app.use((req, res, next) => { if (res.headersSent) return next(); diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index cd6cb263f..e1aaf08e2 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -25,6 +25,8 @@ import { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_GITLAB_API_URL, + INTEGRATION_HASURA_CLOUD, + INTEGRATION_HASURA_CLOUD_API_URL, INTEGRATION_HEROKU, INTEGRATION_HEROKU_API_URL, INTEGRATION_LARAVELFORGE, @@ -47,7 +49,7 @@ import { INTEGRATION_VERCEL, INTEGRATION_VERCEL_API_URL, INTEGRATION_WINDMILL, - INTEGRATION_WINDMILL_API_URL, + INTEGRATION_WINDMILL_API_URL } from "../variables"; import { IIntegrationAuth } from "../models"; import { Octokit } from "@octokit/rest"; @@ -73,7 +75,7 @@ const getApps = async ({ accessToken, accessId, teamId, - workspaceSlug, + workspaceSlug }: { integrationAuth: IIntegrationAuth; accessToken: string; @@ -85,7 +87,7 @@ const getApps = async ({ switch (integrationAuth.integration) { case INTEGRATION_GCP_SECRET_MANAGER: apps = await getAppsGCPSecretManager({ - accessToken, + accessToken }); break; case INTEGRATION_AZURE_KEY_VAULT: @@ -99,50 +101,50 @@ const getApps = async ({ break; case INTEGRATION_HEROKU: apps = await getAppsHeroku({ - accessToken, + accessToken }); break; case INTEGRATION_VERCEL: apps = await getAppsVercel({ integrationAuth, - accessToken, + accessToken }); break; case INTEGRATION_NETLIFY: apps = await getAppsNetlify({ - accessToken, + accessToken }); break; case INTEGRATION_GITHUB: apps = await getAppsGithub({ - accessToken, + accessToken }); break; case INTEGRATION_GITLAB: apps = await getAppsGitlab({ integrationAuth, accessToken, - teamId, + teamId }); break; case INTEGRATION_RENDER: apps = await getAppsRender({ - accessToken, + accessToken }); break; case INTEGRATION_RAILWAY: apps = await getAppsRailway({ - accessToken, + accessToken }); break; case INTEGRATION_FLYIO: apps = await getAppsFlyio({ - accessToken, + accessToken }); break; case INTEGRATION_CIRCLECI: apps = await getAppsCircleCI({ - accessToken, + accessToken }); break; case INTEGRATION_LARAVELFORGE: @@ -154,28 +156,28 @@ const getApps = async ({ case INTEGRATION_TERRAFORM_CLOUD: apps = await getAppsTerraformCloud({ accessToken, - workspacesId: accessId, + workspacesId: accessId }); break; case INTEGRATION_TRAVISCI: apps = await getAppsTravisCI({ - accessToken, + accessToken }); break; case INTEGRATION_TEAMCITY: apps = await getAppsTeamCity({ integrationAuth, - accessToken, + accessToken }); break; case INTEGRATION_SUPABASE: apps = await getAppsSupabase({ - accessToken, + accessToken }); break; case INTEGRATION_CHECKLY: apps = await getAppsCheckly({ - accessToken, + accessToken }); break; case INTEGRATION_CLOUDFLARE_PAGES: @@ -186,7 +188,7 @@ const getApps = async ({ break; case INTEGRATION_NORTHFLANK: apps = await getAppsNorthflank({ - accessToken, + accessToken }); break; case INTEGRATION_BITBUCKET: @@ -197,7 +199,7 @@ const getApps = async ({ break; case INTEGRATION_CODEFRESH: apps = await getAppsCodefresh({ - accessToken, + accessToken }); break; case INTEGRATION_WINDMILL: @@ -206,13 +208,19 @@ const getApps = async ({ }); break; case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM: - apps = await getAppsDigitalOceanAppPlatform({ - accessToken + apps = await getAppsDigitalOceanAppPlatform({ + accessToken }); break; case INTEGRATION_CLOUD_66: apps = await getAppsCloud66({ - accessToken, + accessToken + }); + break; + + case INTEGRATION_HASURA_CLOUD: + apps = await getAppsHasuraCloud({ + accessToken }); break; } @@ -229,73 +237,79 @@ const getApps = async ({ * @returns {String} apps.appId - id of GCP project */ const getAppsGCPSecretManager = async ({ accessToken }: { accessToken: string }) => { - interface GCPApp { projectNumber: string; projectId: string; - lifecycleState: "ACTIVE" | "LIFECYCLE_STATE_UNSPECIFIED" | "DELETE_REQUESTED" | "DELETE_IN_PROGRESS"; + lifecycleState: + | "ACTIVE" + | "LIFECYCLE_STATE_UNSPECIFIED" + | "DELETE_REQUESTED" + | "DELETE_IN_PROGRESS"; name: string; createTime: string; parent: { type: "organization" | "folder" | "project"; id: string; - } + }; } - + interface GCPGetProjectsRes { projects: GCPApp[]; nextPageToken?: string; } - + interface GCPGetServiceRes { name: string; parent: string; - state: "ENABLED" | "DISABLED" | "STATE_UNSPECIFIED" + state: "ENABLED" | "DISABLED" | "STATE_UNSPECIFIED"; } let gcpApps: GCPApp[] = []; const apps: App[] = []; - + const pageSize = 100; let pageToken: string | undefined; let hasMorePages = true; - + while (hasMorePages) { const params = new URLSearchParams({ pageSize: String(pageSize), ...(pageToken ? { pageToken } : {}) }); - const res: GCPGetProjectsRes = (await standardRequest.get(`${INTEGRATION_GCP_API_URL}/v1/projects`, { + const res: GCPGetProjectsRes = ( + await standardRequest.get(`${INTEGRATION_GCP_API_URL}/v1/projects`, { params, headers: { - "Authorization": `Bearer ${accessToken}`, + Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json" } }) - ) - .data; - + ).data; + gcpApps = gcpApps.concat(res.projects); if (!res.nextPageToken) { hasMorePages = false; } - + pageToken = res.nextPageToken; } - + for await (const gcpApp of gcpApps) { try { - const res: GCPGetServiceRes = (await standardRequest.get( - `${INTEGRATION_GCP_SERVICE_USAGE_URL}/v1/projects/${gcpApp.projectId}/services/${INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME}`, { - headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept-Encoding": "application/json" + const res: GCPGetServiceRes = ( + await standardRequest.get( + `${INTEGRATION_GCP_SERVICE_USAGE_URL}/v1/projects/${gcpApp.projectId}/services/${INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } } - } - )).data; - + ) + ).data; + if (res.state === "ENABLED") { apps.push({ name: gcpApp.name, @@ -322,13 +336,13 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { await standardRequest.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { headers: { Accept: "application/vnd.heroku+json; version=3", - Authorization: `Bearer ${accessToken}`, - }, + Authorization: `Bearer ${accessToken}` + } }) ).data; const apps = res.map((a: any) => ({ - name: a.name, + name: a.name })); return apps; @@ -343,7 +357,7 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { */ const getAppsVercel = async ({ integrationAuth, - accessToken, + accessToken }: { integrationAuth: IIntegrationAuth; accessToken: string; @@ -352,21 +366,21 @@ const getAppsVercel = async ({ await standardRequest.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", + "Accept-Encoding": "application/json" }, ...(integrationAuth?.teamId ? { - params: { - teamId: integrationAuth.teamId, - }, - } - : {}), + params: { + teamId: integrationAuth.teamId + } + } + : {}) }) ).data; const apps = res.projects.map((a: any) => ({ name: a.name, - appId: a.id, + appId: a.id })); return apps; @@ -390,24 +404,21 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => { const params = new URLSearchParams({ page: String(page), per_page: String(perPage), - filter: "all", + filter: "all" }); - const { data } = await standardRequest.get( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + const { data } = await standardRequest.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); data.map((a: any) => { apps.push({ name: a.name, - appId: a.site_id, + appId: a.site_id }); }); @@ -441,7 +452,7 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { } const octokit = new Octokit({ - auth: accessToken, + auth: accessToken }); const getAllRepos = async () => { @@ -455,7 +466,7 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { "GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}", { per_page, - page, + page } ); @@ -478,7 +489,7 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { return { appId: a.id, name: a.name, - owner: a.owner.login, + owner: a.owner.login }; }); @@ -499,14 +510,14 @@ const getAppsRender = async ({ accessToken }: { accessToken: string }) => { headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json", - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } }) ).data; const apps = res.map((a: any) => ({ name: a.service.name, - appId: a.service.id, + appId: a.service.id })); return apps; @@ -540,27 +551,27 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => { const { data: { data: { - projects: { edges }, - }, - }, + projects: { edges } + } + } } = await standardRequest.post( INTEGRATION_RAILWAY_API_URL, { query, - variables, + variables }, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); const apps = edges.map((e: any) => ({ name: e.node.name, - appId: e.node.id, + appId: e.node.id })); return apps; @@ -575,7 +586,7 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => { * @returns {String} apps.name - name of Laravel Forge sites * @returns {String} apps.appId - id of Laravel Forge sites */ -const getAppsLaravelForge = async ({ +const getAppsLaravelForge = async ({ accessToken, serverId }: { @@ -583,18 +594,21 @@ const getAppsLaravelForge = async ({ serverId?: string; }) => { const res = ( - await standardRequest.get(`${INTEGRATION_LARAVELFORGE_API_URL}/api/v1/servers/${serverId}/sites`, { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json", - "Content-Type": "application/json", - }, - }) + await standardRequest.get( + `${INTEGRATION_LARAVELFORGE_API_URL}/api/v1/servers/${serverId}/sites`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "Content-Type": "application/json" + } + } + ) ).data.sites; const apps = res.map((a: any) => ({ name: a.name, - appId: a.id, + appId: a.id })); return apps; @@ -613,7 +627,7 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { name: string; hostname: string; } - + const query = ` query($role: String) { apps(type: "container", first: 400, role: $role) { @@ -632,15 +646,15 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { { query, variables: { - role: null, - }, + role: null + } }, { headers: { Authorization: "Bearer " + accessToken, Accept: "application/json", - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ) ).data.data.apps.nodes; @@ -665,14 +679,14 @@ const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => { await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`, { headers: { "Circle-Token": accessToken, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } }) ).data; const apps = res?.map((a: any) => { return { - name: a?.reponame, + name: a?.reponame }; }); @@ -684,15 +698,15 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => { await standardRequest.get(`${INTEGRATION_TRAVISCI_API_URL}/repos`, { headers: { Authorization: `token ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } }) ).data; const apps = res?.map((a: any) => { return { name: a?.slug?.split("/")[1], - appId: a?.id, + appId: a?.id }; }); @@ -707,7 +721,7 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => { * @returns {Object[]} apps - names and ids of Terraform Cloud projects * @returns {String} apps.name - name of Terraform Cloud projects */ -const getAppsTerraformCloud = async ({ +const getAppsTerraformCloud = async ({ accessToken, workspacesId }: { @@ -715,27 +729,29 @@ const getAppsTerraformCloud = async ({ workspacesId?: string; }) => { const res = ( - await standardRequest.get(`${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${workspacesId}`, { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json", - }, - }) + await standardRequest.get( + `${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${workspacesId}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ) ).data.data; - const apps = [] + const apps = []; const appsObj = { - name: res?.attributes.name, - appId: res?.id, + name: res?.attributes.name, + appId: res?.id }; - apps.push(appsObj) + apps.push(appsObj); return apps; }; - /** * Return list of repositories for GitLab integration * @param {Object} obj @@ -746,14 +762,16 @@ const getAppsTerraformCloud = async ({ const getAppsGitlab = async ({ integrationAuth, accessToken, - teamId, + teamId }: { integrationAuth: IIntegrationAuth; accessToken: string; teamId?: string; }) => { - const gitLabApiUrl = integrationAuth.url ? `${integrationAuth.url}/api` : INTEGRATION_GITLAB_API_URL; - + const gitLabApiUrl = integrationAuth.url + ? `${integrationAuth.url}/api` + : INTEGRATION_GITLAB_API_URL; + const apps: App[] = []; let page = 1; @@ -766,24 +784,21 @@ const getAppsGitlab = async ({ while (hasMorePages) { const params = new URLSearchParams({ page: String(page), - per_page: String(perPage), + per_page: String(perPage) }); - const { data } = await standardRequest.get( - `${gitLabApiUrl}/v4/groups/${teamId}/projects`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + const { data } = await standardRequest.get(`${gitLabApiUrl}/v4/groups/${teamId}/projects`, { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); data.map((a: any) => { apps.push({ name: a.name, - appId: a.id, + appId: a.id }); }); @@ -800,32 +815,29 @@ const getAppsGitlab = async ({ await standardRequest.get(`${gitLabApiUrl}/v4/user`, { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } }) ).data; while (hasMorePages) { const params = new URLSearchParams({ page: String(page), - per_page: String(perPage), + per_page: String(perPage) }); - const { data } = await standardRequest.get( - `${gitLabApiUrl}/v4/users/${id}/projects`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + const { data } = await standardRequest.get(`${gitLabApiUrl}/v4/users/${id}/projects`, { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); data.map((a: any) => { apps.push({ name: a.name, - appId: a.id, + appId: a.id }); }); @@ -847,9 +859,9 @@ const getAppsGitlab = async ({ * @returns {Object[]} apps - names and ids of TeamCity projects * @returns {String} apps.name - name of TeamCity projects */ -const getAppsTeamCity = async ({ +const getAppsTeamCity = async ({ integrationAuth, - accessToken, + accessToken }: { integrationAuth: IIntegrationAuth; accessToken: string; @@ -858,15 +870,15 @@ const getAppsTeamCity = async ({ await standardRequest.get(`${integrationAuth.url}/app/rest/projects`, { headers: { Authorization: `Bearer ${accessToken}`, - Accept: "application/json", - }, + Accept: "application/json" + } }) ).data.project.slice(1); - + const apps = res.map((a: any) => { return { name: a.name, - appId: a.id, + appId: a.id }; }); @@ -881,20 +893,17 @@ const getAppsTeamCity = async ({ * @returns {String} apps.name - name of Supabase app */ const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => { - const { data } = await standardRequest.get( - `${INTEGRATION_SUPABASE_API_URL}/v1/projects`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + const { data } = await standardRequest.get(`${INTEGRATION_SUPABASE_API_URL}/v1/projects`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); const apps = data.map((a: any) => { return { name: a.name, - appId: a.id, + appId: a.id }; }); @@ -909,20 +918,17 @@ const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => { * @returns {String} apps.name - name of Checkly account */ const getAppsCheckly = async ({ accessToken }: { accessToken: string }) => { - const { data } = await standardRequest.get( - `${INTEGRATION_CHECKLY_API_URL}/v1/accounts`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, + const { data } = await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/accounts`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" } - ); + }); const apps = data.map((a: any) => { return { name: a.name, - appId: a.id, + appId: a.id }; }); @@ -948,19 +954,19 @@ const getAppsCloudflarePages = async ({ { headers: { Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, + Accept: "application/json" + } } ); const apps = data.result.map((a: any) => { return { name: a.name, - appId: a.id, + appId: a.id }; }); return apps; -} +}; /** * Return list of repositories for the BitBucket integration based on provided BitBucket workspace @@ -970,9 +976,9 @@ const getAppsCloudflarePages = async ({ * @returns {Object[]} apps - BitBucket repositories * @returns {String} apps.name - name of BitBucket repository */ -const getAppsBitBucket = async ({ +const getAppsBitBucket = async ({ accessToken, - workspaceSlug, + workspaceSlug }: { accessToken: string; workspaceSlug?: string; @@ -996,45 +1002,42 @@ const getAppsBitBucket = async ({ } if (!workspaceSlug) { - return [] + return []; } - + const repositories: Repository[] = []; let hasNextPage = true; - let repositoriesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}` + let repositoriesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}`; while (hasNextPage) { - const { data }: { data: RepositoriesResponse } = await standardRequest.get( - repositoriesUrl, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, - } - ); + const { data }: { data: RepositoriesResponse } = await standardRequest.get(repositoriesUrl, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + }); if (data?.values.length > 0) { data.values.forEach((repository) => { - repositories.push(repository) - }) + repositories.push(repository); + }); } if (data.next) { - repositoriesUrl = data.next + repositoriesUrl = data.next; } else { - hasNextPage = false + hasNextPage = false; } } const apps = repositories.map((repository) => { - return { - name: repository.name, - appId: repository.uuid, - }; + return { + name: repository.name, + appId: repository.uuid + }; }); return apps; -} +}; /** Return list of projects for Northflank integration * @param {Object} obj @@ -1045,19 +1048,14 @@ const getAppsBitBucket = async ({ const getAppsNorthflank = async ({ accessToken }: { accessToken: string }) => { const { data: { - data: { - projects - } + data: { projects } } - } = await standardRequest.get( - `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + } = await standardRequest.get(`${INTEGRATION_NORTHFLANK_API_URL}/v1/projects`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); const apps = projects.map((a: any) => { return { @@ -1076,27 +1074,22 @@ const getAppsNorthflank = async ({ accessToken }: { accessToken: string }) => { * @returns {Object[]} apps - names of Supabase apps * @returns {String} apps.name - name of Supabase app */ -const getAppsCodefresh = async ({ - accessToken, -}: { - accessToken: string; -}) => { +const getAppsCodefresh = async ({ accessToken }: { accessToken: string }) => { const res = ( await standardRequest.get(`${INTEGRATION_CODEFRESH_API_URL}/projects`, { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } }) ).data; const apps = res.projects.map((a: any) => ({ name: a.projectName, - appId: a.id, + appId: a.id })); return apps; - }; /** @@ -1107,16 +1100,13 @@ const getAppsCodefresh = async ({ * @returns {String} apps.name - name of Windmill workspace */ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { - const { data } = await standardRequest.get( - `${INTEGRATION_WINDMILL_API_URL}/workspaces/list`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + const { data } = await standardRequest.get(`${INTEGRATION_WINDMILL_API_URL}/workspaces/list`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); - + }); + // check for write access of secrets in windmill workspaces const writeAccessCheck = data.map(async (app: any) => { try { @@ -1134,8 +1124,8 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); @@ -1150,30 +1140,30 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); - + // is write access is allowed then delete the created secrets from workspace if (writeUser && writeFolder) { await standardRequest.delete( - `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/delete/${userPath}`, + `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/delete/${userPath}`, { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); await standardRequest.delete( - `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/delete/${folderPath}`, + `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/delete/${folderPath}`, { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); @@ -1188,16 +1178,16 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { const appsWriteResponses = await Promise.all(writeAccessCheck); const appsWithWriteAccess = appsWriteResponses.filter((appRes: any) => !appRes.error); - + const apps = appsWithWriteAccess.map((a: any) => { return { name: a.name, - appId: a.id, + appId: a.id }; }); - + return apps; -} +}; /** * Return list of applications for DigitalOcean App Platform integration @@ -1225,7 +1215,7 @@ const getAppsDigitalOceanAppPlatform = async ({ accessToken }: { accessToken: st value: string; scope: string; } - + const res = ( await standardRequest.get(`${INTEGRATION_DIGITAL_OCEAN_API_URL}/v2/apps`, { headers: { @@ -1239,8 +1229,28 @@ const getAppsDigitalOceanAppPlatform = async ({ accessToken }: { accessToken: st name: a.spec.name, appId: a.id })); -} - +}; + +const getAppsHasuraCloud = async ({ accessToken }: { accessToken: string }) => { + const res = await standardRequest.post( + INTEGRATION_HASURA_CLOUD_API_URL, + { + query: "query MyQuery { projects { name tenant { id } } }" + }, + { + headers: { + Authorization: `pat ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + + const data = (res?.data?.data?.projects ?? []).map( + ({ name, tenant: { id: appId } }: { name: string; tenant: { id: string } }) => ({ name, appId }) + ); + return data; +}; + /** * Return list of applications for Cloud66 integration * @param {Object} obj @@ -1290,7 +1300,7 @@ const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => { "Accept-Encoding": "application/json" } }) - ).data.response as Cloud66Apps[] + ).data.response as Cloud66Apps[]; const apps = stacks.map((app) => ({ name: app.name, diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index fe5885c9e..5f7d53345 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -32,6 +32,8 @@ import { INTEGRATION_GITLAB, INTEGRATION_GITLAB_API_URL, INTEGRATION_HASHICORP_VAULT, + INTEGRATION_HASURA_CLOUD, + INTEGRATION_HASURA_CLOUD_API_URL, INTEGRATION_HEROKU, INTEGRATION_HEROKU_API_URL, INTEGRATION_LARAVELFORGE, @@ -63,6 +65,10 @@ import { Octokit } from "@octokit/rest"; import _ from "lodash"; import sodium from "libsodium-wrappers"; import { standardRequest } from "../config/request"; +import { + ZGetTenantEnv, + ZUpdateTenantEnv +} from "../validation/hasuraCloudIntegration"; const getSecretKeyValuePair = ( secrets: Record @@ -95,7 +101,7 @@ const syncSecrets = async ({ secrets: Record; accessId: string | null; accessToken: string; - appendices?: { prefix: string, suffix: string }; + appendices?: { prefix: string; suffix: string }; }) => { switch (integration.integration) { case INTEGRATION_GCP_SECRET_MANAGER: @@ -306,6 +312,14 @@ const syncSecrets = async ({ accessToken }); break; + + case INTEGRATION_HASURA_CLOUD: + await syncSecretsHasuraCloud({ + integration, + secrets, + accessToken + }); + break; } }; @@ -963,8 +977,9 @@ const syncSecretsVercel = async ({ : {}), ...(integration?.path ? { - gitBranch: integration?.path - } : {}) + gitBranch: integration?.path + } + : {}) }; const vercelSecrets: VercelSecret[] = ( @@ -992,7 +1007,7 @@ const syncSecretsVercel = async ({ return true; }); - + const res: { [key: string]: VercelSecret } = {}; for await (const vercelSecret of vercelSecrets) { @@ -1352,7 +1367,7 @@ const syncSecretsGitHub = async ({ integration: IIntegration; secrets: Record; accessToken: string; - appendices?: { prefix: string, suffix: string }; + appendices?: { prefix: string; suffix: string }; }) => { interface GitHubRepoKey { key_id: string; @@ -1395,14 +1410,23 @@ const syncSecretsGitHub = async ({ {} ); - encryptedSecrets = Object.keys(encryptedSecrets).reduce((result: { - [key: string]: GitHubSecret; - }, key) => { - if ((appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)) { - result[key] = encryptedSecrets[key]; - } - return result; - }, {}); + encryptedSecrets = Object.keys(encryptedSecrets).reduce( + ( + result: { + [key: string]: GitHubSecret; + }, + key + ) => { + if ( + (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && + (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) + ) { + result[key] = encryptedSecrets[key]; + } + return result; + }, + {} + ); Object.keys(encryptedSecrets).map(async (key) => { if (!(key in secrets)) { @@ -2095,7 +2119,7 @@ const syncSecretsCheckly = async ({ integration: IIntegration; secrets: Record; accessToken: string; - appendices?: { prefix: string, suffix: string }; + appendices?: { prefix: string; suffix: string }; }) => { let getSecretsRes = ( await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { @@ -2113,14 +2137,23 @@ const syncSecretsCheckly = async ({ {} ); - getSecretsRes = Object.keys(getSecretsRes).reduce((result: { - [key: string]: string; - }, key) => { - if ((appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)) { - result[key] = getSecretsRes[key]; - } - return result; - }, {}); + getSecretsRes = Object.keys(getSecretsRes).reduce( + ( + result: { + [key: string]: string; + }, + key + ) => { + if ( + (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && + (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) + ) { + result[key] = getSecretsRes[key]; + } + return result; + }, + {} + ); // add secrets for await (const key of Object.keys(secrets)) { @@ -2195,18 +2228,20 @@ const syncSecretsQovery = async ({ secrets: Record; accessToken: string; }) => { - const getSecretsRes = ( - await standardRequest.get(`${INTEGRATION_QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable`, { - headers: { - Authorization: `Token ${accessToken}`, - "Accept-Encoding": "application/json" + await standardRequest.get( + `${INTEGRATION_QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable`, + { + headers: { + Authorization: `Token ${accessToken}`, + "Accept-Encoding": "application/json" + } } - }) + ) ).data.results.reduce( (obj: any, secret: any) => ({ ...obj, - [secret.key]: {"id": secret.id, "value": secret.value} + [secret.key]: { id: secret.id, value: secret.value } }), {} ); @@ -3076,4 +3111,111 @@ const syncSecretsNorthflank = async ({ ); }; +/** Sync/push [secrets] to Hasura Cloud + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for Hasura Cloud integration + */ +const syncSecretsHasuraCloud = async ({ + integration, + secrets, + accessToken +}: { + integration: IIntegration; + secrets: Record; + accessToken: string; +}) => { + const res = await standardRequest.post( + INTEGRATION_HASURA_CLOUD_API_URL, + { + query: + "query MyQuery($tenantId: uuid!) { getTenantEnv(tenantId: $tenantId) { hash envVars } }", + variables: { + tenantId: integration.appId + } + }, + { + headers: { + Authorization: `pat ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + + const { + data: { + getTenantEnv: { hash, envVars } + } + } = ZGetTenantEnv.parse(res.data); + + let currentHash = hash; + + const secretsToUpdate = Object.keys(secrets).map((key) => { + return ({ + key, + value: secrets[key].value + }); + }); + + if (secretsToUpdate.length) { + // update secrets + + const addRequest = await standardRequest.post( + INTEGRATION_HASURA_CLOUD_API_URL, + { + query: + "mutation MyQuery($currentHash: String!, $envs: [UpdateEnvObject!]!, $tenantId: uuid!) { updateTenantEnv(currentHash: $currentHash, envs: $envs, tenantId: $tenantId) { hash envVars} }", + variables: { + currentHash, + envs: secretsToUpdate, + tenantId: integration.appId + } + }, + { + headers: { + Authorization: `pat ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + + const addRequestResponse = ZUpdateTenantEnv.safeParse(addRequest.data); + if (addRequestResponse.success) { + currentHash = addRequestResponse.data.data.updateTenantEnv.hash; + } + } + + const secretsToDelete = envVars.environment + ? Object.keys(envVars.environment).filter((key) => !(key in secrets)) + : []; + + if (secretsToDelete.length) { + await standardRequest.post( + INTEGRATION_HASURA_CLOUD_API_URL, + { + query: ` + mutation deleteTenantEnv($id: uuid!, $currentHash: String!, $env: [String!]!) { + deleteTenantEnv(tenantId: $id, currentHash: $currentHash, deleteEnvs: $env) { + hash + envVars + } + } + `, + variables: { + id: integration.appId, + currentHash, + env: secretsToDelete + } + }, + { + headers: { + Authorization: `pat ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + } +}; + export { syncSecrets }; diff --git a/backend/src/interfaces/services/SecretService/index.ts b/backend/src/interfaces/services/SecretService/index.ts index 203f0e178..a2d9c5bb5 100644 --- a/backend/src/interfaces/services/SecretService/index.ts +++ b/backend/src/interfaces/services/SecretService/index.ts @@ -44,6 +44,7 @@ export interface GetSecretParams { export interface UpdateSecretParams { secretName: string; newSecretName?: string; + secretId?: string; secretKeyCiphertext?: string; secretKeyIV?: string; secretKeyTag?: string; diff --git a/backend/src/middleware/requireMfaAuth.ts b/backend/src/middleware/requireMfaAuth.ts index 7a7f7db4b..9c5313b05 100644 --- a/backend/src/middleware/requireMfaAuth.ts +++ b/backend/src/middleware/requireMfaAuth.ts @@ -2,7 +2,8 @@ import jwt from "jsonwebtoken"; import { NextFunction, Request, Response } from "express"; import { User } from "../models"; import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; -import { getJwtMfaSecret } from "../config"; +import { getAuthSecret } from "../config"; +import { AuthTokenType } from "../variables"; declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -26,8 +27,10 @@ const requireMfaAuth = async ( if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: "Missing Authorization Body in the request header"})) const decodedToken = ( - jwt.verify(AUTH_TOKEN_VALUE, await getJwtMfaSecret()) + jwt.verify(AUTH_TOKEN_VALUE, await getAuthSecret()) ); + + if (decodedToken.authTokenType !== AuthTokenType.MFA_TOKEN) throw UnauthorizedRequestError(); const user = await User.findOne({ _id: decodedToken.userId, diff --git a/backend/src/middleware/requireSignupAuth.ts b/backend/src/middleware/requireSignupAuth.ts index 3c5c48d12..510cb3d03 100644 --- a/backend/src/middleware/requireSignupAuth.ts +++ b/backend/src/middleware/requireSignupAuth.ts @@ -2,7 +2,8 @@ import jwt from "jsonwebtoken"; import { NextFunction, Request, Response } from "express"; import { User } from "../models"; import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; -import { getJwtSignupSecret } from "../config"; +import { getAuthSecret } from "../config"; +import { AuthTokenType } from "../variables"; declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -27,8 +28,10 @@ const requireSignupAuth = async ( if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: "Missing Authorization Body in the request header"})) const decodedToken = ( - jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret()) + jwt.verify(AUTH_TOKEN_VALUE, await getAuthSecret()) ); + + if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw UnauthorizedRequestError(); const user = await User.findOne({ _id: decodedToken.userId, diff --git a/backend/src/models/apiKeyDataV2.ts b/backend/src/models/apiKeyDataV2.ts new file mode 100644 index 000000000..6775a0878 --- /dev/null +++ b/backend/src/models/apiKeyDataV2.ts @@ -0,0 +1,38 @@ +import { Document, Schema, Types, model } from "mongoose"; + +export interface IAPIKeyDataV2 extends Document { + _id: Types.ObjectId; + name: string; + user: Types.ObjectId; + lastUsed?: Date + usageCount: number; + expiresAt?: Date; +} + +const apiKeyDataV2Schema = new Schema( + { + name: { + type: String, + required: true + }, + user: { + type: Schema.Types.ObjectId, + ref: "User", + required: true + }, + lastUsed: { + type: Date, + required: false + }, + usageCount: { + type: Number, + default: 0, + required: true + } + }, + { + timestamps: true + } +); + +export const APIKeyDataV2 = model("APIKeyDataV2", apiKeyDataV2Schema); \ No newline at end of file diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 99fc9c4f1..6bd1ebef6 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -24,9 +24,10 @@ export * from "./user"; export * from "./userAction"; export * from "./workspace"; export * from "./serviceTokenData"; // TODO: deprecate -export * from "./apiKeyData"; +export * from "./serviceTokenDataV3"; +export * from "./serviceTokenDataV3Key"; +export * from "./apiKeyData"; // TODO: deprecate +export * from "./apiKeyDataV2"; export * from "./loginSRPDetail"; export * from "./tokenVersion"; export * from "./webhooks"; -export * from "./serviceTokenDataV3"; -export * from "./serviceTokenDataV3Key"; diff --git a/backend/src/models/integration/integration.ts b/backend/src/models/integration/integration.ts index 7b9957393..8c997a28d 100644 --- a/backend/src/models/integration/integration.ts +++ b/backend/src/models/integration/integration.ts @@ -14,6 +14,7 @@ import { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_HASHICORP_VAULT, + INTEGRATION_HASURA_CLOUD, INTEGRATION_HEROKU, INTEGRATION_LARAVELFORGE, INTEGRATION_NETLIFY, @@ -76,7 +77,8 @@ export interface IIntegration { | "cloud-66" | "northflank" | "windmill" - | "gcp-secret-manager"; + | "gcp-secret-manager" + | "hasura-cloud"; integrationAuth: Types.ObjectId; metadata: Metadata; } @@ -86,67 +88,67 @@ const integrationSchema = new Schema( workspace: { type: Schema.Types.ObjectId, ref: "Workspace", - required: true, + required: true }, environment: { type: String, - required: true, + required: true }, isActive: { type: Boolean, - required: true, + required: true }, url: { // for custom self-hosted integrations (e.g. self-hosted GitHub enterprise) type: String, - default: null, + default: null }, app: { // name of app in provider type: String, - default: null, + default: null }, appId: { // id of app in provider type: String, - default: null, + default: null }, targetEnvironment: { // target environment type: String, - default: null, + default: null }, targetEnvironmentId: { type: String, - default: null, + default: null }, targetService: { // railway-specific service // qovery-specific project type: String, - default: null, + default: null }, targetServiceId: { // railway-specific service // qovery specific project type: String, - default: null, + default: null }, owner: { // github-specific repo owner-login type: String, - default: null, + default: null }, path: { // aws-parameter-store-specific path // (also) vercel preview-branch type: String, - default: null, + default: null }, region: { // aws-parameter-store-specific path type: String, - default: null, + default: null }, scope: { // qovery-specific scope @@ -183,19 +185,20 @@ const integrationSchema = new Schema( INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CLOUD_66, INTEGRATION_NORTHFLANK, - INTEGRATION_GCP_SECRET_MANAGER + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_HASURA_CLOUD ], - required: true, + required: true }, integrationAuth: { type: Schema.Types.ObjectId, ref: "IntegrationAuth", - required: true, + required: true }, secretPath: { type: String, required: true, - default: "/", + default: "/" }, metadata: { type: Schema.Types.Mixed, @@ -203,8 +206,8 @@ const integrationSchema = new Schema( } }, { - timestamps: true, + timestamps: true } ); -export const Integration = model("Integration", integrationSchema); \ No newline at end of file +export const Integration = model("Integration", integrationSchema); diff --git a/backend/src/models/integrationAuth/integrationAuth.ts b/backend/src/models/integrationAuth/integrationAuth.ts index 312ee09d7..e65c126af 100644 --- a/backend/src/models/integrationAuth/integrationAuth.ts +++ b/backend/src/models/integrationAuth/integrationAuth.ts @@ -1,205 +1,203 @@ import { - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_BASE64, - ENCODING_SCHEME_UTF8, - INTEGRATION_AWS_PARAMETER_STORE, - INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_BITBUCKET, - INTEGRATION_CIRCLECI, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CLOUD_66, - INTEGRATION_CODEFRESH, - INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_FLYIO, - INTEGRATION_GCP_SECRET_MANAGER, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_HASHICORP_VAULT, - INTEGRATION_HEROKU, - INTEGRATION_LARAVELFORGE, - INTEGRATION_NETLIFY, - INTEGRATION_NORTHFLANK, - INTEGRATION_RAILWAY, - INTEGRATION_RENDER, - INTEGRATION_SUPABASE, - INTEGRATION_TEAMCITY, - INTEGRATION_TERRAFORM_CLOUD, - INTEGRATION_TRAVISCI, - INTEGRATION_VERCEL, - INTEGRATION_WINDMILL - } from "../../variables"; - import { Document, Schema, Types, model } from "mongoose"; - import { IntegrationAuthMetadata } from "./types"; - - export interface IIntegrationAuth extends Document { - _id: Types.ObjectId; - workspace: Types.ObjectId; - integration: - | "heroku" - | "vercel" - | "netlify" - | "github" - | "gitlab" - | "render" - | "railway" - | "flyio" - | "azure-key-vault" - | "laravel-forge" - | "circleci" - | "travisci" - | "supabase" - | "aws-parameter-store" - | "aws-secret-manager" - | "checkly" - | "qovery" - | "cloudflare-pages" - | "codefresh" - | "digital-ocean-app-platform" - | "bitbucket" - | "cloud-66" - | "terraform-cloud" - | "teamcity" - | "northflank" - | "windmill" - | "gcp-secret-manager"; - teamId: string; - accountId: string; - url: string; - namespace: string; - refreshCiphertext?: string; - refreshIV?: string; - refreshTag?: string; - accessIdCiphertext?: string; - accessIdIV?: string; - accessIdTag?: string; - accessCiphertext?: string; - accessIV?: string; - accessTag?: string; - algorithm?: "aes-256-gcm"; - keyEncoding?: "utf8" | "base64"; - accessExpiresAt?: Date; - metadata?: IntegrationAuthMetadata; - } - - const integrationAuthSchema = new Schema( - { - workspace: { - type: Schema.Types.ObjectId, - ref: "Workspace", - required: true, - }, - integration: { - type: String, - enum: [ - INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_AWS_PARAMETER_STORE, - INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_RAILWAY, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_LARAVELFORGE, - INTEGRATION_TRAVISCI, - INTEGRATION_TEAMCITY, - INTEGRATION_SUPABASE, - INTEGRATION_TERRAFORM_CLOUD, - INTEGRATION_HASHICORP_VAULT, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CODEFRESH, - INTEGRATION_WINDMILL, - INTEGRATION_BITBUCKET, - INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_CLOUD_66, - INTEGRATION_NORTHFLANK, - INTEGRATION_GCP_SECRET_MANAGER - ], - required: true, - }, - teamId: { - // vercel-specific integration param - type: String, - }, - url: { - // for any self-hosted integrations (e.g. self-hosted hashicorp-vault) - type: String, - }, - namespace: { - // hashicorp-vault-specific integration param - type: String, - }, - accountId: { - // netlify-specific integration param - type: String, - }, - refreshCiphertext: { - type: String, - select: false, - }, - refreshIV: { - type: String, - select: false, - }, - refreshTag: { - type: String, - select: false, - }, - accessIdCiphertext: { - type: String, - select: false, - }, - accessIdIV: { - type: String, - select: false, - }, - accessIdTag: { - type: String, - select: false, - }, - accessCiphertext: { - type: String, - select: false, - }, - accessIV: { - type: String, - select: false, - }, - accessTag: { - type: String, - select: false, - }, - accessExpiresAt: { - type: Date, - select: false, - }, - algorithm: { // the encryption algorithm used - type: String, - enum: [ALGORITHM_AES_256_GCM], - required: true, - }, - keyEncoding: { - type: String, - enum: [ - ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64, - ], - required: true, - }, - metadata: { - type: Schema.Types.Mixed - } + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_BASE64, + ENCODING_SCHEME_UTF8, + INTEGRATION_AWS_PARAMETER_STORE, + INTEGRATION_AWS_SECRET_MANAGER, + INTEGRATION_AZURE_KEY_VAULT, + INTEGRATION_BITBUCKET, + INTEGRATION_CIRCLECI, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CLOUD_66, + INTEGRATION_CODEFRESH, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, + INTEGRATION_FLYIO, + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_GITHUB, + INTEGRATION_GITLAB, + INTEGRATION_HASHICORP_VAULT, + INTEGRATION_HASURA_CLOUD, + INTEGRATION_HEROKU, + INTEGRATION_LARAVELFORGE, + INTEGRATION_NETLIFY, + INTEGRATION_NORTHFLANK, + INTEGRATION_RAILWAY, + INTEGRATION_RENDER, + INTEGRATION_SUPABASE, + INTEGRATION_TEAMCITY, + INTEGRATION_TERRAFORM_CLOUD, + INTEGRATION_TRAVISCI, + INTEGRATION_VERCEL, + INTEGRATION_WINDMILL +} from "../../variables"; +import { Document, Schema, Types, model } from "mongoose"; +import { IntegrationAuthMetadata } from "./types"; + +export interface IIntegrationAuth extends Document { + _id: Types.ObjectId; + workspace: Types.ObjectId; + integration: + | "heroku" + | "vercel" + | "netlify" + | "github" + | "gitlab" + | "render" + | "railway" + | "flyio" + | "azure-key-vault" + | "laravel-forge" + | "circleci" + | "travisci" + | "supabase" + | "aws-parameter-store" + | "aws-secret-manager" + | "checkly" + | "qovery" + | "cloudflare-pages" + | "codefresh" + | "digital-ocean-app-platform" + | "bitbucket" + | "cloud-66" + | "terraform-cloud" + | "teamcity" + | "northflank" + | "windmill" + | "gcp-secret-manager" + | "hasura-cloud"; + teamId: string; + accountId: string; + url: string; + namespace: string; + refreshCiphertext?: string; + refreshIV?: string; + refreshTag?: string; + accessIdCiphertext?: string; + accessIdIV?: string; + accessIdTag?: string; + accessCiphertext?: string; + accessIV?: string; + accessTag?: string; + algorithm?: "aes-256-gcm"; + keyEncoding?: "utf8" | "base64"; + accessExpiresAt?: Date; + metadata?: IntegrationAuthMetadata; +} + +const integrationAuthSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true }, - { - timestamps: true, + integration: { + type: String, + enum: [ + INTEGRATION_AZURE_KEY_VAULT, + INTEGRATION_AWS_PARAMETER_STORE, + INTEGRATION_AWS_SECRET_MANAGER, + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_GITLAB, + INTEGRATION_RENDER, + INTEGRATION_RAILWAY, + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, + INTEGRATION_LARAVELFORGE, + INTEGRATION_TRAVISCI, + INTEGRATION_TEAMCITY, + INTEGRATION_SUPABASE, + INTEGRATION_TERRAFORM_CLOUD, + INTEGRATION_HASHICORP_VAULT, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CODEFRESH, + INTEGRATION_WINDMILL, + INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, + INTEGRATION_CLOUD_66, + INTEGRATION_NORTHFLANK, + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_HASURA_CLOUD + ], + required: true + }, + teamId: { + // vercel-specific integration param + type: String + }, + url: { + // for any self-hosted integrations (e.g. self-hosted hashicorp-vault) + type: String + }, + namespace: { + // hashicorp-vault-specific integration param + type: String + }, + accountId: { + // netlify-specific integration param + type: String + }, + refreshCiphertext: { + type: String, + select: false + }, + refreshIV: { + type: String, + select: false + }, + refreshTag: { + type: String, + select: false + }, + accessIdCiphertext: { + type: String, + select: false + }, + accessIdIV: { + type: String, + select: false + }, + accessIdTag: { + type: String, + select: false + }, + accessCiphertext: { + type: String, + select: false + }, + accessIV: { + type: String, + select: false + }, + accessTag: { + type: String, + select: false + }, + accessExpiresAt: { + type: Date, + select: false + }, + algorithm: { + // the encryption algorithm used + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true + }, + keyEncoding: { + type: String, + enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], + required: true + }, + metadata: { + type: Schema.Types.Mixed } - ); - - export const IntegrationAuth = model( - "IntegrationAuth", - integrationAuthSchema - ); \ No newline at end of file + }, + { + timestamps: true + } +); + +export const IntegrationAuth = model("IntegrationAuth", integrationAuthSchema); diff --git a/backend/src/models/serviceTokenDataV3.ts b/backend/src/models/serviceTokenDataV3.ts index a9758422e..c9895402f 100644 --- a/backend/src/models/serviceTokenDataV3.ts +++ b/backend/src/models/serviceTokenDataV3.ts @@ -54,6 +54,7 @@ const serviceTokenDataV3Schema = new Schema( }, isActive: { type: Boolean, + default: true, required: true }, lastUsed: { diff --git a/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts b/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts index b18d7bccc..490b31c10 100644 --- a/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts +++ b/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts @@ -40,9 +40,9 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => { const prefix = (integration.metadata?.secretPrefix || ""); const suffix = (integration.metadata?.secretSuffix || ""); const newKey = prefix + key + suffix; - + suffixedSecrets[newKey] = secrets[key]; - } + } } const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth); @@ -67,7 +67,7 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => { }) syncSecretsToThirdPartyServices.on("error", (error) => { - console.log("QUEUE ERROR:", error) // eslint-disable-line + // console.log("QUEUE ERROR:", error) // eslint-disable-line }) export const syncSecretsToActiveIntegrationsQueue = (jobDetails: TSyncSecretsToThirdPartyServices) => { diff --git a/backend/src/routes/v1/secretImps.ts b/backend/src/routes/v1/secretImps.ts index 1d2696d60..5dba7fc9d 100644 --- a/backend/src/routes/v1/secretImps.ts +++ b/backend/src/routes/v1/secretImps.ts @@ -7,7 +7,7 @@ import { AuthMode } from "../../variables"; router.post( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] }), secretImpsController.createSecretImp ); @@ -15,7 +15,7 @@ router.post( router.put( "/:id", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] }), secretImpsController.updateSecretImport ); @@ -23,7 +23,7 @@ router.put( router.delete( "/:id", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] }), secretImpsController.deleteSecretImport ); @@ -31,7 +31,7 @@ router.delete( router.get( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] }), secretImpsController.getSecretImports ); @@ -39,7 +39,7 @@ router.get( router.get( "/secrets", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] }), secretImpsController.getAllSecretsFromImport ); diff --git a/backend/src/routes/v1/secretsFolder.ts b/backend/src/routes/v1/secretsFolder.ts index 697c4d3c9..ed296373c 100644 --- a/backend/src/routes/v1/secretsFolder.ts +++ b/backend/src/routes/v1/secretsFolder.ts @@ -12,7 +12,7 @@ import { AuthMode } from "../../variables"; router.post( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] }), createFolder ); @@ -20,7 +20,7 @@ router.post( router.patch( "/:folderName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] }), updateFolderById ); @@ -28,7 +28,7 @@ router.patch( router.delete( "/:folderName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] }), deleteFolder ); @@ -36,7 +36,7 @@ router.delete( router.get( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY] }), getFolders ); diff --git a/backend/src/routes/v2/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts index 611cf6003..c959cfa56 100644 --- a/backend/src/routes/v2/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -6,7 +6,7 @@ import { import { AuthMode } from "../../variables"; import { serviceTokenDataController } from "../../controllers/v2"; -router.get( +router.get( // TODO: deprecate (moving to ST V3) "/", requireAuth({ acceptedAuthModes: [AuthMode.SERVICE_TOKEN] @@ -14,7 +14,7 @@ router.get( serviceTokenDataController.getServiceTokenData ); -router.post( +router.post( // TODO: deprecate (moving to ST V3) "/", requireAuth({ acceptedAuthModes: [AuthMode.JWT] @@ -22,7 +22,7 @@ router.post( serviceTokenDataController.createServiceTokenData ); -router.delete( +router.delete( // TODO: deprecate (moving to ST V3) "/:serviceTokenDataId", requireAuth({ acceptedAuthModes: [AuthMode.JWT] @@ -30,4 +30,4 @@ router.delete( serviceTokenDataController.deleteServiceTokenData ); -export default router; +export default router; \ No newline at end of file diff --git a/backend/src/routes/v2/users.ts b/backend/src/routes/v2/users.ts index cc7a7ec0d..54c16898f 100644 --- a/backend/src/routes/v2/users.ts +++ b/backend/src/routes/v2/users.ts @@ -36,7 +36,7 @@ router.get( usersController.getMyOrganizations ); -router.get( +router.get( // TODO: deprecate (moving to API Key V2) "/me/api-keys", requireAuth({ acceptedAuthModes: [AuthMode.JWT] diff --git a/backend/src/routes/v3/index.ts b/backend/src/routes/v3/index.ts index 1a95439ab..a2b64294c 100644 --- a/backend/src/routes/v3/index.ts +++ b/backend/src/routes/v3/index.ts @@ -1,10 +1,12 @@ import auth from "./auth"; +import users from "./users"; import secrets from "./secrets"; import workspaces from "./workspaces"; import signup from "./signup"; export { auth, + users, secrets, signup, workspaces diff --git a/backend/src/routes/v3/users.ts b/backend/src/routes/v3/users.ts new file mode 100644 index 000000000..f465791f8 --- /dev/null +++ b/backend/src/routes/v3/users.ts @@ -0,0 +1,15 @@ +import express from "express"; +const router = express.Router(); +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; +import { usersController } from "../../controllers/v3"; + +router.get( + "/me/api-keys", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + usersController.getMyAPIKeys +); + +export default router; \ No newline at end of file diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 6f46824d4..c1c0e6ac5 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -13,6 +13,7 @@ import { } from "../models"; import { createToken } from "../helpers/auth"; import { + getAuthSecret, getClientIdGitHubLogin, getClientIdGitLabLogin, getClientIdGoogleLogin, @@ -20,13 +21,12 @@ import { getClientSecretGitLabLogin, getClientSecretGoogleLogin, getJwtProviderAuthLifetime, - getJwtProviderAuthSecret, getSiteURL, getUrlGitLabLogin } from "../config"; import { getSSOConfigHelper } from "../ee/helpers/organizations"; import { InternalServerError, OrganizationNotFoundError } from "./errors"; -import { ACCEPTED, INTEGRATION_GITHUB_API_URL, INVITED, MEMBER } from "../variables"; +import { ACCEPTED, AuthTokenType, INTEGRATION_GITHUB_API_URL, INVITED, MEMBER } from "../variables"; import { standardRequest } from "../config/request"; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -131,6 +131,7 @@ const initializePassport = async () => { const isUserCompleted = !!user.publicKey; const providerAuthToken = createToken({ payload: { + authTokenType: AuthTokenType.PROVIDER_TOKEN, userId: user._id.toString(), email: user.email, firstName: user.firstName, @@ -143,7 +144,7 @@ const initializePassport = async () => { } : {}) }, expiresIn: await getJwtProviderAuthLifetime(), - secret: await getJwtProviderAuthSecret(), + secret: await getAuthSecret(), }); req.isUserCompleted = isUserCompleted; @@ -204,6 +205,7 @@ const initializePassport = async () => { const isUserCompleted = !!user.publicKey; const providerAuthToken = createToken({ payload: { + authTokenType: AuthTokenType.PROVIDER_TOKEN, userId: user._id.toString(), email: user.email, firstName: user.firstName, @@ -216,7 +218,7 @@ const initializePassport = async () => { } : {}) }, expiresIn: await getJwtProviderAuthLifetime(), - secret: await getJwtProviderAuthSecret(), + secret: await getAuthSecret(), }); req.isUserCompleted = isUserCompleted; @@ -258,6 +260,7 @@ const initializePassport = async () => { const isUserCompleted = !!user.publicKey; const providerAuthToken = createToken({ payload: { + authTokenType: AuthTokenType.PROVIDER_TOKEN, userId: user._id.toString(), email: user.email, firstName: user.firstName, @@ -270,7 +273,7 @@ const initializePassport = async () => { } : {}) }, expiresIn: await getJwtProviderAuthLifetime(), - secret: await getJwtProviderAuthSecret(), + secret: await getAuthSecret(), }); req.isUserCompleted = isUserCompleted; @@ -291,8 +294,7 @@ const initializePassport = async () => { }); interface ISAMLConfig { - path: string; - callbackURL: string; + callbackUrl: string; entryPoint: string; issuer: string; cert: string; @@ -301,8 +303,7 @@ const initializePassport = async () => { } const samlConfig: ISAMLConfig = ({ - path: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`, - callbackURL: `${await getSiteURL()}/api/v1/sso/saml2${ssoIdentifier}`, + callbackUrl: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`, entryPoint: ssoConfig.entryPoint, issuer: ssoConfig.issuer, cert: ssoConfig.cert, @@ -313,6 +314,12 @@ const initializePassport = async () => { samlConfig.wantAuthnResponseSigned = false; } + if (ssoConfig.authProvider.toString() === AuthMethod.AZURE_SAML.toString()) { + if (req.body.RelayState && JSON.parse(req.body.RelayState).spInitiated) { + samlConfig.audience = `spn:${ssoConfig.issuer}`; + } + } + req.ssoConfig = ssoConfig; done(null, samlConfig); @@ -397,6 +404,7 @@ const initializePassport = async () => { const isUserCompleted = !!user.publicKey; const providerAuthToken = createToken({ payload: { + authTokenType: AuthTokenType.PROVIDER_TOKEN, userId: user._id.toString(), email: user.email, firstName, @@ -405,11 +413,11 @@ const initializePassport = async () => { authMethod: req.ssoConfig.authProvider, isUserCompleted, ...(req.body.RelayState ? { - callbackPort: req.body.RelayState as string + callbackPort: JSON.parse(req.body.RelayState).callbackPort as string } : {}) }, expiresIn: await getJwtProviderAuthLifetime(), - secret: await getJwtProviderAuthSecret(), + secret: await getAuthSecret(), }); req.isUserCompleted = isUserCompleted; diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index d85d63aca..6625bff7b 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -55,9 +55,6 @@ export const setup = async () => { // initializing global feature set await EELicenseService.initGlobalFeatureSet(); - // initializing the database connection - await DatabaseService.initDatabase(await getMongoURL()); - await initializePassport(); // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY diff --git a/backend/src/validation/apiKeyDataV3.ts b/backend/src/validation/apiKeyDataV3.ts new file mode 100644 index 000000000..c92ce468c --- /dev/null +++ b/backend/src/validation/apiKeyDataV3.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +export const CreateAPIKeyV3 = z.object({ + body: z.object({ + name: z.string().trim() + }) +}); + +export const UpdateAPIKeyV3 = z.object({ + params: z.object({ + apiKeyDataId: z.string().trim() + }), + body: z.object({ + name: z.string().trim() + }) +}); + +export const DeleteAPIKeyV3 = z.object({ + params: z.object({ + apiKeyDataId: z.string().trim() + }) +}); \ No newline at end of file diff --git a/backend/src/validation/hasuraCloudIntegration.ts b/backend/src/validation/hasuraCloudIntegration.ts new file mode 100644 index 000000000..63b037370 --- /dev/null +++ b/backend/src/validation/hasuraCloudIntegration.ts @@ -0,0 +1,21 @@ +import * as z from "zod"; + +export const ZGetTenantEnv = z.object({ + data: z.object({ + getTenantEnv: z.object({ + hash: z.string(), + envVars: z.object({ + environment: z.record(z.any()).optional() + }) + }) + }) +}); + +export const ZUpdateTenantEnv = z.object({ + data: z.object({ + updateTenantEnv: z.object({ + hash: z.string(), + envVars: z.record(z.any()) + }) + }) +}); diff --git a/backend/src/validation/index.ts b/backend/src/validation/index.ts index a823f8095..447948c5a 100644 --- a/backend/src/validation/index.ts +++ b/backend/src/validation/index.ts @@ -10,3 +10,4 @@ export * from "./secrets"; export * from "./serviceAccount"; export * from "./serviceTokenData"; export * from "./serviceTokenDataV3"; +export * from "./apiKeyDataV3"; diff --git a/backend/src/validation/secrets.ts b/backend/src/validation/secrets.ts index 264eb7271..174ffa791 100644 --- a/backend/src/validation/secrets.ts +++ b/backend/src/validation/secrets.ts @@ -353,6 +353,7 @@ export const UpdateSecretByNameV3 = z.object({ body: z.object({ workspaceId: z.string().trim(), environment: z.string().trim(), + secretId: z.string().trim().optional(), type: z.enum([SECRET_SHARED, SECRET_PERSONAL]), secretPath: z.string().trim().default("/"), secretValueCiphertext: z.string().trim(), diff --git a/backend/src/variables/authentication.ts b/backend/src/variables/authentication.ts index 1376c977e..30ba4bf14 100644 --- a/backend/src/variables/authentication.ts +++ b/backend/src/variables/authentication.ts @@ -1,3 +1,12 @@ +export enum AuthTokenType { + ACCESS_TOKEN = "accessToken", + REFRESH_TOKEN = "refreshToken", + SIGNUP_TOKEN = "signupToken", + MFA_TOKEN = "mfaToken", + PROVIDER_TOKEN = "providerToken", + API_KEY = "apiKey" +} + export enum AuthMode { JWT = "jwt", SERVICE_TOKEN = "serviceToken", diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index e6e20e73c..a70429082 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -1,12 +1,12 @@ import { - getClientIdAzure, - getClientIdBitBucket, - getClientIdGCPSecretManager, - getClientIdGitHub, - getClientIdGitLab, - getClientIdHeroku, - getClientIdNetlify, - getClientSlugVercel + getClientIdAzure, + getClientIdBitBucket, + getClientIdGCPSecretManager, + getClientIdGitHub, + getClientIdGitLab, + getClientIdHeroku, + getClientIdNetlify, + getClientSlugVercel } from "../config"; // integrations @@ -22,7 +22,7 @@ export const INTEGRATION_GITLAB = "gitlab"; export const INTEGRATION_RENDER = "render"; export const INTEGRATION_RAILWAY = "railway"; export const INTEGRATION_FLYIO = "flyio"; -export const INTEGRATION_LARAVELFORGE = "laravel-forge" +export const INTEGRATION_LARAVELFORGE = "laravel-forge"; export const INTEGRATION_CIRCLECI = "circleci"; export const INTEGRATION_TRAVISCI = "travisci"; export const INTEGRATION_TEAMCITY = "teamcity"; @@ -38,32 +38,34 @@ export const INTEGRATION_WINDMILL = "windmill"; export const INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform"; export const INTEGRATION_CLOUD_66 = "cloud-66"; export const INTEGRATION_NORTHFLANK = "northflank"; +export const INTEGRATION_HASURA_CLOUD = "hasura-cloud"; export const INTEGRATION_SET = new Set([ - INTEGRATION_GCP_SECRET_MANAGER, - INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_LARAVELFORGE, - INTEGRATION_TRAVISCI, - INTEGRATION_TEAMCITY, - INTEGRATION_SUPABASE, - INTEGRATION_CHECKLY, - INTEGRATION_QOVERY, - INTEGRATION_TERRAFORM_CLOUD, - INTEGRATION_HASHICORP_VAULT, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CODEFRESH, - INTEGRATION_WINDMILL, - INTEGRATION_BITBUCKET, - INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_CLOUD_66, - INTEGRATION_NORTHFLANK + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_AZURE_KEY_VAULT, + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_GITLAB, + INTEGRATION_RENDER, + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, + INTEGRATION_LARAVELFORGE, + INTEGRATION_TRAVISCI, + INTEGRATION_TEAMCITY, + INTEGRATION_SUPABASE, + INTEGRATION_CHECKLY, + INTEGRATION_QOVERY, + INTEGRATION_TERRAFORM_CLOUD, + INTEGRATION_HASHICORP_VAULT, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CODEFRESH, + INTEGRATION_WINDMILL, + INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, + INTEGRATION_CLOUD_66, + INTEGRATION_NORTHFLANK, + INTEGRATION_HASURA_CLOUD ]); // integration types @@ -71,15 +73,14 @@ export const INTEGRATION_OAUTH2 = "oauth2"; // integration oauth endpoints export const INTEGRATION_GCP_TOKEN_URL = "https://oauth2.googleapis.com/token"; -export const INTEGRATION_AZURE_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; +export const INTEGRATION_AZURE_TOKEN_URL = + "https://login.microsoftonline.com/common/oauth2/v2.0/token"; export const INTEGRATION_HEROKU_TOKEN_URL = "https://id.heroku.com/oauth/token"; -export const INTEGRATION_VERCEL_TOKEN_URL = - "https://api.vercel.com/v2/oauth/access_token"; +export const INTEGRATION_VERCEL_TOKEN_URL = "https://api.vercel.com/v2/oauth/access_token"; export const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token"; -export const INTEGRATION_GITHUB_TOKEN_URL = - "https://github.com/login/oauth/access_token"; +export const INTEGRATION_GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"; export const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token"; -export const INTEGRATION_BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token" +export const INTEGRATION_BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token"; // integration apps endpoints export const INTEGRATION_GCP_API_URL = "https://cloudresourcemanager.googleapis.com"; @@ -106,268 +107,279 @@ export const INTEGRATION_WINDMILL_API_URL = "https://app.windmill.dev/api"; export const INTEGRATION_DIGITAL_OCEAN_API_URL = "https://api.digitalocean.com"; export const INTEGRATION_CLOUD_66_API_URL = "https://app.cloud66.com/api"; export const INTEGRATION_NORTHFLANK_API_URL = "https://api.northflank.com"; +export const INTEGRATION_HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql"; -export const INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com" +export const INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com"; export const INTEGRATION_GCP_SECRET_MANAGER_URL = `https://${INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME}`; export const INTEGRATION_GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com"; -export const INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; +export const INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE = + "https://www.googleapis.com/auth/cloud-platform"; export const getIntegrationOptions = async () => { - const INTEGRATION_OPTIONS = [ - { - name: "Heroku", - slug: "heroku", - image: "Heroku.png", - isAvailable: true, - type: "oauth", - clientId: await getClientIdHeroku(), - docsLink: "", - }, - { - name: "Vercel", - slug: "vercel", - image: "Vercel.png", - isAvailable: true, - type: "oauth", - clientId: "", - clientSlug: await getClientSlugVercel(), - docsLink: "", - }, - { - name: "Netlify", - slug: "netlify", - image: "Netlify.png", - isAvailable: true, - type: "oauth", - clientId: await getClientIdNetlify(), - docsLink: "", - }, - { - name: "GitHub", - slug: "github", - image: "GitHub.png", - isAvailable: true, - type: "oauth", - clientId: await getClientIdGitHub(), - docsLink: "", - }, - { - name: "Render", - slug: "render", - image: "Render.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Railway", - slug: "railway", - image: "Railway.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Fly.io", - slug: "flyio", - image: "Flyio.svg", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "AWS Parameter Store", - slug: "aws-parameter-store", - image: "Amazon Web Services.png", - isAvailable: true, - type: "custom", - clientId: "", - docsLink: "", - }, - { - name: "Laravel Forge", - slug: "laravel-forge", - image: "Laravel Forge.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "AWS Secrets Manager", - slug: "aws-secret-manager", - image: "Amazon Web Services.png", - isAvailable: true, - type: "custom", - clientId: "", - docsLink: "", - }, - { - name: "Azure Key Vault", - slug: "azure-key-vault", - image: "Microsoft Azure.png", - isAvailable: true, - type: "oauth", - clientId: await getClientIdAzure(), - docsLink: "", - }, - { - name: "Circle CI", - slug: "circleci", - image: "Circle CI.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "GitLab", - slug: "gitlab", - image: "GitLab.png", - isAvailable: true, - type: "custom", - clientId: await getClientIdGitLab(), - docsLink: "", - }, - { - name: "Terraform Cloud", - slug: "terraform-cloud", - image: "Terraform Cloud.png", - isAvailable: true, - type: "pat", - cliendId: "", - docsLink: "", - }, - { - name: "Travis CI", - slug: "travisci", - image: "Travis CI.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "TeamCity", - slug: "teamcity", - image: "TeamCity.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Supabase", - slug: "supabase", - image: "Supabase.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Checkly", - slug: "checkly", - image: "Checkly.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Qovery", - slug: "qovery", - image: "Qovery.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "HashiCorp Vault", - slug: "hashicorp-vault", - image: "Vault.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "GCP Secret Manager", - slug: "gcp-secret-manager", - image: "Google Cloud Platform.png", - isAvailable: true, - type: "oauth", - clientId: await getClientIdGCPSecretManager(), - docsLink: "" - }, - { - name: "Cloudflare Pages", - slug: "cloudflare-pages", - image: "Cloudflare.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "" - }, - { - name: "BitBucket", - slug: "bitbucket", - image: "BitBucket.png", - isAvailable: true, - type: "oauth", - clientId: await getClientIdBitBucket(), - docsLink: "" - }, - { - name: "Codefresh", - slug: "codefresh", - image: "Codefresh.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Windmill", - slug: "windmill", - image: "Windmill.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Digital Ocean App Platform", - slug: "digital-ocean-app-platform", - image: "Digital Ocean.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Cloud 66", - slug: "cloud-66", - image: "Cloud 66.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Northflank", - slug: "northflank", - image: "Northflank.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "" - }, - ] - - return INTEGRATION_OPTIONS; -} + const INTEGRATION_OPTIONS = [ + { + name: "Heroku", + slug: "heroku", + image: "Heroku.png", + isAvailable: true, + type: "oauth", + clientId: await getClientIdHeroku(), + docsLink: "" + }, + { + name: "Vercel", + slug: "vercel", + image: "Vercel.png", + isAvailable: true, + type: "oauth", + clientId: "", + clientSlug: await getClientSlugVercel(), + docsLink: "" + }, + { + name: "Netlify", + slug: "netlify", + image: "Netlify.png", + isAvailable: true, + type: "oauth", + clientId: await getClientIdNetlify(), + docsLink: "" + }, + { + name: "GitHub", + slug: "github", + image: "GitHub.png", + isAvailable: true, + type: "oauth", + clientId: await getClientIdGitHub(), + docsLink: "" + }, + { + name: "Render", + slug: "render", + image: "Render.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Railway", + slug: "railway", + image: "Railway.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Fly.io", + slug: "flyio", + image: "Flyio.svg", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "AWS Parameter Store", + slug: "aws-parameter-store", + image: "Amazon Web Services.png", + isAvailable: true, + type: "custom", + clientId: "", + docsLink: "" + }, + { + name: "Laravel Forge", + slug: "laravel-forge", + image: "Laravel Forge.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "AWS Secrets Manager", + slug: "aws-secret-manager", + image: "Amazon Web Services.png", + isAvailable: true, + type: "custom", + clientId: "", + docsLink: "" + }, + { + name: "Azure Key Vault", + slug: "azure-key-vault", + image: "Microsoft Azure.png", + isAvailable: true, + type: "oauth", + clientId: await getClientIdAzure(), + docsLink: "" + }, + { + name: "Circle CI", + slug: "circleci", + image: "Circle CI.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "GitLab", + slug: "gitlab", + image: "GitLab.png", + isAvailable: true, + type: "custom", + clientId: await getClientIdGitLab(), + docsLink: "" + }, + { + name: "Terraform Cloud", + slug: "terraform-cloud", + image: "Terraform Cloud.png", + isAvailable: true, + type: "pat", + cliendId: "", + docsLink: "" + }, + { + name: "Travis CI", + slug: "travisci", + image: "Travis CI.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "TeamCity", + slug: "teamcity", + image: "TeamCity.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Supabase", + slug: "supabase", + image: "Supabase.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Checkly", + slug: "checkly", + image: "Checkly.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Qovery", + slug: "qovery", + image: "Qovery.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "HashiCorp Vault", + slug: "hashicorp-vault", + image: "Vault.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "GCP Secret Manager", + slug: "gcp-secret-manager", + image: "Google Cloud Platform.png", + isAvailable: true, + type: "oauth", + clientId: await getClientIdGCPSecretManager(), + docsLink: "" + }, + { + name: "Cloudflare Pages", + slug: "cloudflare-pages", + image: "Cloudflare.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "BitBucket", + slug: "bitbucket", + image: "BitBucket.png", + isAvailable: true, + type: "oauth", + clientId: await getClientIdBitBucket(), + docsLink: "" + }, + { + name: "Codefresh", + slug: "codefresh", + image: "Codefresh.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Windmill", + slug: "windmill", + image: "Windmill.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Digital Ocean App Platform", + slug: "digital-ocean-app-platform", + image: "Digital Ocean.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Cloud 66", + slug: "cloud-66", + image: "Cloud 66.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Northflank", + slug: "northflank", + image: "Northflank.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Hasura Cloud", + slug: "hasura-cloud", + image: "Hasura.svg", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + } + ]; + + return INTEGRATION_OPTIONS; +}; diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 251ffde3e..5c4d314f5 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc. package cmd import ( + "crypto/sha256" "encoding/base64" "fmt" "regexp" @@ -11,8 +12,6 @@ import ( "strings" "unicode" - "crypto/sha256" - "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/crypto" "github.com/Infisical/infisical-merge/packages/models" @@ -441,6 +440,11 @@ func generateExampleEnv(cmd *cobra.Command, args []string) { } } + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + infisicalToken, err := cmd.Flags().GetString("token") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -451,7 +455,7 @@ func generateExampleEnv(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}) if err != nil { util.HandleError(err, "To fetch all secrets") } @@ -650,8 +654,8 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod } func init() { - secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") + secretsGenerateExampleEnvCmd.Flags().String("path", "/", "Fetch secrets from within a folder path") secretsCmd.AddCommand(secretsGenerateExampleEnvCmd) secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") diff --git a/docker-compose.yml b/docker-compose.yml index c6108ea7f..57e5b5b63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,46 +1,20 @@ version: "3" services: - nginx: - container_name: infisical-nginx - image: nginx - restart: always - ports: - - 80:80 - - 443:443 - volumes: - - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro - depends_on: - - frontend - - backend - networks: - - infisical - backend: container_name: infisical-backend restart: unless-stopped depends_on: - mongo - image: infisical/backend + image: infisical/infisical:latest env_file: .env + ports: + - 80:8080 environment: - NODE_ENV=production networks: - infisical - frontend: - container_name: infisical-frontend - restart: unless-stopped - depends_on: - - backend - image: infisical/frontend - env_file: .env - environment: - # - NEXT_PUBLIC_POSTHOG_API_KEY=${POSTHOG_PROJECT_API_KEY} - - INFISICAL_TELEMETRY_ENABLED=${TELEMETRY_ENABLED} - networks: - - infisical - redis: image: redis container_name: infisical-dev-redis diff --git a/docs/CONTRIBUTING.MD b/docs/CONTRIBUTING.MD new file mode 100644 index 000000000..a74f0985e --- /dev/null +++ b/docs/CONTRIBUTING.MD @@ -0,0 +1,24 @@ +# Contributing to the documentation + +## Getting familiar with Mintlify +New to Mintlify. [Start Here](https://mintlify.com/docs/quickstart) + + +## ๐Ÿ‘ฉโ€๐Ÿ’ป Development + +Install the [Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the documentation changes locally. To install, use the following command + +``` +npm i -g mintlify +``` + +Run the following command at the root of your documentation (where mint.json is) + +``` +mintlify dev +``` + +## Troubleshooting + +- Mintlify dev isn't running - Run `mintlify install` it'll re-install dependencies. +- Page loads as a 404 - Make sure you are running in a folder with `mint.json`. Check the `/docs` folder diff --git a/docs/documentation/platform/sso/azure.mdx b/docs/documentation/platform/sso/azure.mdx index 7c082b971..b280feaa9 100644 --- a/docs/documentation/platform/sso/azure.mdx +++ b/docs/documentation/platform/sso/azure.mdx @@ -63,12 +63,17 @@ description: "Configure Azure SAML for Infisical SSO" 7. Get IdP values: -Back in the **Set up Single Sign-On with SAML** screen, copy the **Login URL**, **Azure AD Identifier** and **SAML Certificate** to use when finishing configuring Azure SAML in Infisical. +In the **Set up Single Sign-On with SAML** screen, copy the **Login URL** and **SAML Certificate** to use when finishing configuring Azure SAML in Infisical. -Back in Infisical, set **Login URL** and **Azure AD Identifier** from above. Once you've done that, press **Update** to complete the required configuration. +![Azure SAML identity provider values 1](../../../images/sso/azure/idp-values.png) -![Azure SAML identity provider values](../../../images/sso/azure/idp-values.png) -![Azure SAML paste identity provider values](../../../images/sso/azure/idp-values-2.png) +In the **Properties** screen, copy the **Application ID** to use when finishing configuring Azure SAML in Infisical. + +![Azure SAML identity provider values 2](../../../images/sso/azure/idp-values-2.png) + +Back in Infisical, set **Login URL**, **Azure Application ID**, and **SAML Certificate** from above. Once you've done that, press **Update** to complete the required configuration. + +![Azure SAML paste identity provider values](../../../images/sso/azure/idp-values-3.png) When pasting the certificate into Infisical, you'll want to retain `-----BEGIN diff --git a/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-auth.png b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-auth.png new file mode 100644 index 000000000..585f9fcb3 Binary files /dev/null and b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-auth.png differ diff --git a/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-create.png b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-create.png new file mode 100644 index 000000000..eca04f08b Binary files /dev/null and b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-create.png differ diff --git a/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-tokens.png b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-tokens.png new file mode 100644 index 000000000..6c45c0b19 Binary files /dev/null and b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-tokens.png differ diff --git a/docs/images/integrations/hasura-cloud/integrations-hasura-cloud.png b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud.png new file mode 100644 index 000000000..855cef3ce Binary files /dev/null and b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud.png differ diff --git a/docs/images/sso/azure/idp-values-2.png b/docs/images/sso/azure/idp-values-2.png index e95b1781c..30b87da12 100644 Binary files a/docs/images/sso/azure/idp-values-2.png and b/docs/images/sso/azure/idp-values-2.png differ diff --git a/docs/images/sso/azure/idp-values-3.png b/docs/images/sso/azure/idp-values-3.png new file mode 100644 index 000000000..d534f1b76 Binary files /dev/null and b/docs/images/sso/azure/idp-values-3.png differ diff --git a/docs/integrations/cloud/hasura-cloud.mdx b/docs/integrations/cloud/hasura-cloud.mdx new file mode 100644 index 000000000..770852742 --- /dev/null +++ b/docs/integrations/cloud/hasura-cloud.mdx @@ -0,0 +1,36 @@ +--- +title: "Hasura Cloud" +description: "How to sync secrets from Infisical to Hasura Cloud" +--- + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Enter your Hasura Cloud Access Token + +Obtain a Hasura Cloud Access Token in My Account > Access Tokens + +![integrations hasura cloud tokens](../../images/integrations/hasura-cloud/integrations-hasura-cloud-tokens.png) + +Press on the Hasura Cloud tile and input your Hasura Cloud access token to grant Infisical access to your Hasura Cloud account. + +![integrations hasura cloud authorization](../../images/integrations/hasura-cloud/integrations-hasura-cloud-auth.png) + + + If this is your project's first cloud integration, then you'll have to grant + Infisical access to your project's environment variables. Although this step + breaks E2EE, it's necessary for Infisical to sync the environment variables to + the cloud platform. + + +## Start integration + +Select which Infisical environment secrets you want to sync to which Hasura Cloud project and press create integration to start syncing secrets to Hasura Cloud. + +![integrations hasura cloud](../../images/integrations/hasura-cloud/integrations-hasura-cloud-create.png) +![integrations hasura cloud](../../images/integrations/hasura-cloud/integrations-hasura-cloud.png) diff --git a/docs/mint.json b/docs/mint.json index 9ed8b8ab4..92876198d 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -141,10 +141,10 @@ "group": "Deployment options", "pages": [ "self-hosting/overview", + "self-hosting/deployment-options/standalone-infisical", "self-hosting/deployment-options/kubernetes-helm", "self-hosting/deployment-options/aws-ec2", "self-hosting/deployment-options/docker-compose", - "self-hosting/deployment-options/standalone-infisical", "self-hosting/deployment-options/digital-ocean-marketplace" ] }, @@ -189,9 +189,7 @@ }, { "group": "Integrations", - "pages": [ - "integrations/overview" - ] + "pages": ["integrations/overview"] }, { "group": "Infrastructure Integrations", @@ -221,9 +219,7 @@ }, { "group": "Digital Ocean", - "pages": [ - "integrations/cloud/digital-ocean-app-platform" - ] + "pages": ["integrations/cloud/digital-ocean-app-platform"] }, "integrations/cloud/heroku", "integrations/cloud/vercel", @@ -234,6 +230,7 @@ "integrations/cloud/laravel-forge", "integrations/cloud/supabase", "integrations/cloud/northflank", + "integrations/cloud/hasura-cloud", "integrations/cloud/terraform-cloud", "integrations/cloud/teamcity", "integrations/cloud/cloudflare-pages", @@ -277,9 +274,7 @@ }, { "group": "Build Tool Integrations", - "pages": [ - "integrations/build-tools/gradle" - ] + "pages": ["integrations/build-tools/gradle"] }, { "group": "Overview", diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index e348f5b3e..9b054e376 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -3,7 +3,7 @@ title: "All environment variables" description: "Configure your environment variables when self-hosting Infisical." --- -## Backend environment variables +## Environment variables Depending on your chosen self hosted deployment method, you may need to configured at least the required environment variable listed below. Other environment variables are listed below to increase the functionality of your self hosted instance based on your use case. @@ -11,43 +11,12 @@ Other environment variables are listed below to increase the functionality of yo - Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16` - + Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16` + -{" "} - - - Must be a random 16 byte hex string. Can be generated with `openssl rand -hex - 16` - - -{" "} - - - Must be a random 16 byte hex string. Can be generated with `openssl rand -hex - 16` - - -{" "} - - - Must be a random 16 byte hex string. Can be generated with `openssl rand -hex - 16` - - -{" "} - - - Must be a random 16 byte hex string. Can be generated with `openssl rand -hex - 16` - - -{" "} - - - Must be a random 16 byte hex string. Can be generated with `openssl rand -hex - 16` - + + Must be a random 32 byte base64 string. Can be generated with `openssl rand -base64 32` + *TLS based connection string is not yet supported @@ -140,9 +109,6 @@ Other environment variables are listed below to increase the functionality of yo To integrate with external auth providers, provide value for the related keys - - Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16` - OAuth2 client ID for Google login @@ -232,12 +198,3 @@ Infisical uses Sentry to report error logs - -## Frontend environment variables - - diff --git a/docs/self-hosting/deployment-options/fly.io.mdx b/docs/self-hosting/deployment-options/fly.io.mdx index 4f43c9f46..b8c561224 100644 --- a/docs/self-hosting/deployment-options/fly.io.mdx +++ b/docs/self-hosting/deployment-options/fly.io.mdx @@ -31,7 +31,7 @@ primary_region = "iad" MONGO_URL = <> [http_service] - internal_port = 80 + internal_port = 8080 ``` diff --git a/docs/self-hosting/deployment-options/kubernetes-helm.mdx b/docs/self-hosting/deployment-options/kubernetes-helm.mdx index 37383fa82..e0a4f36e6 100644 --- a/docs/self-hosting/deployment-options/kubernetes-helm.mdx +++ b/docs/self-hosting/deployment-options/kubernetes-helm.mdx @@ -23,40 +23,27 @@ helm repo update ## Add Helm values -Create a values.yaml file to configure various installation settings, such as the docker image tags and environment variables for both the frontend and backend. To explore all configurable properties for your values file, [visit this page](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical). +Create a values.yaml file to configure various installation settings, such as the docker image tags and environment variables. To explore all configurable properties for your values file, [visit this page](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical). #### Set image tags -By default, the application will use the latest tag to retrieve the required Docker images, which may be appropriate for most cases. -However, it's important to specify a particular version of Infisical during installation to prevent any significant updates from disrupting your deployment. -View [properties for frontend and backend](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical#parameters). - +By default, the application will use the `latest` docker image tag. This is okay for test environments; however, for production deployments it is important to pin your deployment to a particular docker image tag to prevent receiving unintended changes. - To find the latest version number of Infisical, follow the links below - - [frontend Docker image](https://hub.docker.com/r/infisical/frontend/tags) - - [backend Docker image](https://hub.docker.com/r/infisical/backend/tags) + To find the latest version number of Infisical, click [here](https://hub.docker.com/r/infisical/infisical/tags) ```yaml simple-values-example.yaml -frontend: - name: frontend - replicaCount: 2 - image: - repository: infisical/frontend - tag: "v0.34.2" # <--- frontend version - pullPolicy: Always - backend: replicaCount: 2 image: - repository: infisical/backend - tag: "v0.34.2" # <--- backend version + repository: infisical/infisical + tag: "v0.39.5" pullPolicy: Always ``` #### Configure environment variables -You can configure environment variables for the frontend and backend in your Helm values file under the property `frontendEnvironmentVariables` and `backendEnvironmentVariables` respectively. View configurable [environment variables](../configuration/envars). +You can configure environment variables for your instance of Infisical though the Helm values file under the property `backendEnvironmentVariables`. View configurable [environment variables](../configuration/envars). Infisical requires the following backend environment variables to be defined: _`ENCRYPTION_KEY`_, _`JWT_SIGNUP_SECRET`_, _`JWT_REFRESH_SECRET`_, _`JWT_AUTH_SECRET`_, _`JWT_MFA_SECRET`_ and _`JWT_SERVICE_SECRET`_. @@ -87,38 +74,30 @@ Infisical uses Nginx to route external traffic. You can install Nginx along with ... ingress: nginx: - enabled: false #<-- if you would like to install nginx along with Infisical + enabled: true #<-- if you would like to install nginx along with Infisical ``` #### Database -Infisical uses a document database as its persistence layer. With this Helm chart, you spin up a MongoDB instance power by Bitnami along side other Infisical services in your cluster. +Infisical uses a MongoDB as its persistence layer. With this Helm chart, a MongoDB instance is automatically spun up for use with Infisical. When persistence is enabled, the data will be stored as Kubernetes Persistence Volume. View all [properties for mongodb](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical). ```yaml simple-values-example.yaml mongodb: - enabled: false + enabled: true persistence: enabled: false ``` -To increase data redundancy, we recommend that you use a managed document database service such as AWS Document DB, MongoDB or similar services instead. +To achieve high availability and data redundancy, we recommend that you use a managed document database service such as AWS Document DB, MongoDB or similar services instead of the in cluster database. Managed database connection string can be set in the `backendEnvironmentVariables`. #### Example helm values ```yaml simple-values-example.yaml -frontend: - name: frontend - replicaCount: 2 - image: - repository: infisical/frontend - tag: "v0.34.2" # <--- frontend version - pullPolicy: Always - backend: replicaCount: 2 image: - repository: infisical/backend - tag: "v0.34.2" # <--- backend version + repository: infisical/infisical + tag: "v0.39.5" pullPolicy: Always backendEnvironmentVariables: @@ -126,7 +105,7 @@ backendEnvironmentVariables: ingress: nginx: - enabled: true #<-- if you would like to install nginx along with Infisical + enabled: true ``` @@ -136,22 +115,6 @@ ingress: nginx: enabled: true - frontend: - enabled: true - name: frontend - podAnnotations: {} - deploymentAnnotations: {} - replicaCount: 4 - image: - repository: infisical/frontend - tag: "v0.34.2" # <--- frontend version - pullPolicy: IfNotPresent - kubeSecretRef: null - service: - annotations: {} - type: ClusterIP - nodePort: "" - backend: enabled: true name: backend @@ -159,8 +122,8 @@ ingress: deploymentAnnotations: {} replicaCount: 4 image: - repository: infisical/backend - tag: "v0.34.2" # <--- backend version + repository: infisical/infisical + tag: "v0.39.5" pullPolicy: IfNotPresent kubeSecretRef: null service: @@ -176,26 +139,9 @@ ingress: ## Mongo DB persistence mongodb: - enabled: false - persistence: - enabled: false - - ingress: enabled: true - annotations: - cert-manager.io/cluster-issuer: "letsencrypt-prod" # <-- if you are setting up HTTPS - hostName: app.infisical.com ## <- Replace with your own domain - frontend: - path: / - pathType: Prefix - backend: - path: /api - pathType: Prefix - tls: # <-- if you are setting up HTTPS - - secretName: echo-tls - hosts: - - app.infisical.com - + persistence: + enabled: true ``` diff --git a/docs/self-hosting/deployment-options/standalone-infisical.mdx b/docs/self-hosting/deployment-options/standalone-infisical.mdx index 03f132d69..26abf6c93 100644 --- a/docs/self-hosting/deployment-options/standalone-infisical.mdx +++ b/docs/self-hosting/deployment-options/standalone-infisical.mdx @@ -3,11 +3,15 @@ title: "Docker" description: "Learn to install Infisical purely on docker" --- -The Infisical standalone version combines all the essential components of the application into a single container, making deployment and management more straightforward than using Kubernetes or Docker Compose. +The Infisical standalone version combines all the essential components into a single container, making deployment and management more straightforward than other methods. -Since all the components are bundled into one image, running this version of Infisical requires a minimum of **230MB of memory**. +## Prerequisites -This guide assumes you have basic knowledge of Docker and have it installed on your system. If you don't have Docker installed, please follow the official installation guide: https://docs.docker.com/get-docker/ +This guide assumes you have basic knowledge of Docker and have it installed on your system. If you don't have Docker installed, please follow the official installation guide [here](https://docs.docker.com/get-docker/). + +#### System requirements +To have a functional deployment, we recommended compute with **2GB of RAM** and **1 CPU**. +However, depending on your usage, you may need to further scale up system resources to meet demand. ## Pull the Infisical Docker image @@ -18,59 +22,39 @@ docker pull infisical/infisical:latest ``` ## Run with docker -The Infisical Docker image requires several required environment variables. -Add the required environment variables listed below to your docker run command. View [all configurable environment variables](../configuration/envars) - +To run Infisical, we'll need to configure the required configs listed below. +Other configs can be found [here](../configuration/envars) Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16` - + Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16` - - Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16` - - - - Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16` - - - - Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16` - - - - Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16` - - - - Redis connection string - - + A MongoDB connection string. Can use any MongoDB PaaS such as Mongo Atlas, AWS Document DB, etc. *TLS based connection string is not yet supported + + Redis connection string. Only required if you plan to use web integrations. + + Once you have added the required environment variables to your docker run command, execute it in your terminal. ```bash -docker run -p 80:80 \ +docker run -p 80:8080 \ -e ENCRYPTION_KEY=f40c9178624764ad85a6830b37ce239a \ --e JWT_SIGNUP_SECRET=38ea90fb7998b92176080f457d890392 \ --e JWT_REFRESH_SECRET=7764c7bbf3928ad501591a3e005eb364 \ --e JWT_AUTH_SECRET=5239fea3a4720c0e524f814a540e14a2 \ --e JWT_SERVICE_SECRET=8509fb8b90c9b53e9e61d1e35826dcb5 \ +-e AUTH_SECRET=5239fea3a4720c0e524f814a540e14a2 \ -e MONGO_URL="<>" \ --e REDIS_URL="<>" \ infisical/infisical:latest ``` - The sample environment variables listed above are only to be used as an example and should not be used in production + The above environment variable values are only to be used as an example and should not be used in production ## Verify the installation: diff --git a/docs/self-hosting/overview.mdx b/docs/self-hosting/overview.mdx index 382ae9567..8e72c5c01 100644 --- a/docs/self-hosting/overview.mdx +++ b/docs/self-hosting/overview.mdx @@ -8,11 +8,11 @@ Self-hosted Infisical allows you to maintain your sensitive information within y Choose from a variety of deployment options listed below to get started. - Use our Helm chart to Install Infisical on your Kubernetes cluster + Use the fully packaged docker image to deploy Infisical anywhere - Use the fully packaged, single docker image Infisical to deploy anywhere - + title="Kubernetes" + color="#ea5a0c" + href="deployment-options/kubernetes-helm" +> + Use our Helm chart to Install Infisical on your Kubernetes cluster + diff --git a/ecosystem.config.js b/ecosystem.config.js deleted file mode 100644 index 5c8cc6832..000000000 --- a/ecosystem.config.js +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - apps: [ - { - name: 'frontend', - script: "./scripts/start.sh", - instances: 1, - cwd: "./app", - interpreter: 'sh', - exec_mode: "fork", - autorestart: true, - watch: false, - max_memory_restart: '500M', - }, - { - name: 'backend', - script: 'npm', - args: 'run start', - cwd: "./backend", - instances: 1, - exec_mode: "fork", - autorestart: true, - watch: false, - max_memory_restart: '500M', - }, - { - name: "nginx", - script: "nginx", - args: "-g 'daemon off;'", - exec_interpreter: "none", - }, - ], -}; \ No newline at end of file diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 692fecd97..6e74f20a6 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -8,7 +8,7 @@ module.exports = { env: { browser: true, es2021: true, - "es6": true + es6: true }, extends: [ "airbnb", diff --git a/frontend/next.config.js b/frontend/next.config.js index 51cf8f308..9d894694f 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -1,8 +1,4 @@ -// @ts-check -/** - * @type {import('next').NextConfig} - **/ const path = require("path"); const ContentSecurityPolicy = ` @@ -53,7 +49,9 @@ const securityHeaders = [ value: ContentSecurityPolicy.replace(/\s{2,}/g, " ").trim() } ]; - +/** + * @type {import('next').NextConfig} + **/ module.exports = { output: "standalone", i18n: { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f97070490..6a8026255 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,5 +1,5 @@ { - "name": "npm-proj-1695919945735-0.225773463026700768rr1Oh", + "name": "frontend", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 9e1bfb99d..b40c55d55 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -6,74 +6,75 @@ const integrationSlugNameMapping: Mapping = { "azure-key-vault": "Azure Key Vault", "aws-parameter-store": "AWS Parameter Store", "aws-secret-manager": "AWS Secrets Manager", - "heroku": "Heroku", - "vercel": "Vercel", - "netlify": "Netlify", - "github": "GitHub", - "gitlab": "GitLab", - "render": "Render", + heroku: "Heroku", + vercel: "Vercel", + netlify: "Netlify", + github: "GitHub", + gitlab: "GitLab", + render: "Render", "laravel-forge": "Laravel Forge", - "railway": "Railway", - "flyio": "Fly.io", - "circleci": "CircleCI", - "travisci": "TravisCI", - "supabase": "Supabase", - "checkly": "Checkly", - "qovery": "Qovery", + railway: "Railway", + flyio: "Fly.io", + circleci: "CircleCI", + travisci: "TravisCI", + supabase: "Supabase", + checkly: "Checkly", + qovery: "Qovery", "terraform-cloud": "Terraform Cloud", - "teamcity": "TeamCity", + teamcity: "TeamCity", "hashicorp-vault": "Vault", "cloudflare-pages": "Cloudflare Pages", - "codefresh": "Codefresh", + codefresh: "Codefresh", "digital-ocean-app-platform": "Digital Ocean App Platform", - "bitbucket": "BitBucket", + bitbucket: "BitBucket", "cloud-66": "Cloud 66", - "northflank": "Northflank", - "windmill": "Windmill", - "gcp-secret-manager": "GCP Secret Manager" -} + northflank: "Northflank", + windmill: "Windmill", + "gcp-secret-manager": "GCP Secret Manager", + "hasura-cloud": "Hasura Cloud" +}; const envMapping: Mapping = { Development: "dev", Staging: "staging", Production: "prod", - Testing: "test", + Testing: "test" }; const reverseEnvMapping: Mapping = { dev: "Development", staging: "Staging", prod: "Production", - test: "Testing", + test: "Testing" }; const contextNetlifyMapping: Mapping = { - "dev": "Local development", + dev: "Local development", "branch-deploy": "Branch deploys", "deploy-preview": "Deploy Previews", - "production": "Production" -} + production: "Production" +}; const reverseContextNetlifyMapping: Mapping = { "Local development": "dev", "Branch deploys": "branch-deploy", "Deploy Previews": "deploy-preview", - "Production": "production" -} + Production: "production" +}; const plansDev: Mapping = { - "starter": "prod_Mb4ATFT5QAHoPM", - "team": "prod_NEpD2WMXUS2eDn", - "professional": "prod_Mb4CetZ2jE7jdl", - "enterprise": "licence_key_required" -} + starter: "prod_Mb4ATFT5QAHoPM", + team: "prod_NEpD2WMXUS2eDn", + professional: "prod_Mb4CetZ2jE7jdl", + enterprise: "licence_key_required" +}; const plansProd: Mapping = { - "starter": "prod_Mb8oR5XNwyFTul", - "team": "prod_NEp7fAB3UJWK6A", - "professional": "prod_Mb8pUIpA0OUi5N", - "enterprise": "licence_key_required" -} + starter: "prod_Mb8oR5XNwyFTul", + team: "prod_NEp7fAB3UJWK6A", + professional: "prod_Mb8pUIpA0OUi5N", + enterprise: "licence_key_required" +}; const plans = plansProd || plansDev; @@ -83,4 +84,5 @@ export { integrationSlugNameMapping, plans, reverseContextNetlifyMapping, - reverseEnvMapping} + reverseEnvMapping +}; diff --git a/frontend/public/images/integrations/Hasura.svg b/frontend/public/images/integrations/Hasura.svg new file mode 100644 index 000000000..732f821cb --- /dev/null +++ b/frontend/public/images/integrations/Hasura.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/scripts/initialize-standalone-build.sh b/frontend/scripts/initialize-standalone-build.sh new file mode 100755 index 000000000..d9138bb77 --- /dev/null +++ b/frontend/scripts/initialize-standalone-build.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY" "$NEXT_PUBLIC_POSTHOG_API_KEY" + +scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID" + +if [ "$TELEMETRY_ENABLED" != "false" ]; then + echo "Telemetry is enabled" + scripts/set-standalone-build-telemetry.sh true +else + echo "Client opted out of telemetry" + scripts/set-standalone-build-telemetry.sh false +fi diff --git a/frontend/scripts/replace-standalone-build-variable.sh b/frontend/scripts/replace-standalone-build-variable.sh new file mode 100755 index 000000000..fde4ca282 --- /dev/null +++ b/frontend/scripts/replace-standalone-build-variable.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +ORIGINAL=$1 +REPLACEMENT=$2 + +if [ "${ORIGINAL}" = "${REPLACEMENT}" ]; then + echo "Environment variable replacement is the same, skipping.." + exit 0 +fi + +echo "Replacing pre-baked value.." + +find public .next -type f -name "*.js" | +while read file; do + sed -i "s|$ORIGINAL|$REPLACEMENT|g" "$file" +done diff --git a/frontend/scripts/replace-variable.sh b/frontend/scripts/replace-variable.sh old mode 100644 new mode 100755 diff --git a/frontend/scripts/set-standalone-build-telemetry.sh b/frontend/scripts/set-standalone-build-telemetry.sh new file mode 100644 index 000000000..5e788b6b9 --- /dev/null +++ b/frontend/scripts/set-standalone-build-telemetry.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +VALUE=$1 + +find public .next -type f -name "*.js" | +while read file; do + sed -i "s|TELEMETRY_CAPTURING_ENABLED|$VALUE|g" "$file" +done diff --git a/frontend/scripts/set-telemetry.sh b/frontend/scripts/set-telemetry.sh old mode 100644 new mode 100755 diff --git a/frontend/src/components/analytics/posthog.ts b/frontend/src/components/analytics/posthog.ts index 706f79515..246d5c9cc 100644 --- a/frontend/src/components/analytics/posthog.ts +++ b/frontend/src/components/analytics/posthog.ts @@ -10,7 +10,7 @@ export const initPostHog = () => { try { if (typeof window !== "undefined") { // @ts-ignore - if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED) { + if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === "true") { posthog.init(POSTHOG_API_KEY, { api_host: POSTHOG_HOST }); diff --git a/frontend/src/components/utilities/telemetry/Telemetry.ts b/frontend/src/components/utilities/telemetry/Telemetry.ts index 2676bed85..76e86fa37 100644 --- a/frontend/src/components/utilities/telemetry/Telemetry.ts +++ b/frontend/src/components/utilities/telemetry/Telemetry.ts @@ -13,7 +13,7 @@ class Capturer { } capture(item: string) { - if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) { + if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED === "true") { try { this.api.capture(item); } catch (error) { @@ -23,7 +23,7 @@ class Capturer { } identify(id: string, email?: string) { - if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) { + if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED === "true") { try { this.api.identify(id, { email: email diff --git a/frontend/src/components/v2/Alert/Alert.stories.tsx b/frontend/src/components/v2/Alert/Alert.stories.tsx new file mode 100644 index 000000000..49e8ca43c --- /dev/null +++ b/frontend/src/components/v2/Alert/Alert.stories.tsx @@ -0,0 +1,61 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { Alert, AlertDescription } from "./Alert"; + +const meta: Meta = { + title: "Components/Alert", + component: Alert, + tags: ["v2"] +}; + +export default meta; + +type Story = StoryObj; + +const ExampleComponent = () => this is a description; + +export const Default: Story = { + args: { + children: + } +}; + +export const Warning: Story = { + args: { + children: , + variant: "warning" + } +}; + +export const Danger: Story = { + args: { + children: , + variant: "danger" + } +}; + +export const WithCustomIcon: Story = { + args: { + children: , + variant: "warning", + icon: + } +}; + +export const WithOutIcon: Story = { + args: { + children: , + variant: "warning", + icon: null + } +}; + +export const WithOutTitle: Story = { + args: { + children: , + variant: "warning", + hideTitle: true + } +}; diff --git a/frontend/src/components/v2/Alert/Alert.tsx b/frontend/src/components/v2/Alert/Alert.tsx new file mode 100644 index 000000000..9de25193c --- /dev/null +++ b/frontend/src/components/v2/Alert/Alert.tsx @@ -0,0 +1,85 @@ +import { forwardRef } from "react"; +import { + faExclamationCircle, + faExclamationTriangle, + faInfoCircle +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { type VariantProps, cva } from "cva"; +import { twMerge } from "tailwind-merge"; + +const alertVariants = cva( + "w-full bg-mineshaft-800 rounded-lg border px-4 py-3 text-sm flex items-center gap-x-4", + { + variants: { + variant: { + default: "", + danger: "text-red border-red", + warning: "text-yellow border-yellow" + } + }, + defaultVariants: { + variant: "default" + } + } +); + +type AlertProps = { + title?: string; + hideTitle?: boolean; + icon?: React.ReactNode; +}; + +const variantTitleMap = { + default: "Info", + danger: "Danger", + warning: "Warning" +}; + +const variantIconMap = { + default: faInfoCircle, + danger: faExclamationCircle, + warning: faExclamationTriangle +}; + +const Alert = forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps & AlertProps +>(({ className, variant, title, icon, hideTitle = false, children, ...props }, ref) => { + const defaultTitle = title ?? variantTitleMap[variant ?? "default"]; + return ( +
+
+ {typeof icon !== "undefined" ? ( + <>{icon} + ) : ( + + )} +
+
+ {hideTitle ? null : ( +
+ {defaultTitle} +
+ )} + {children} +
+
+ ); +}); +Alert.displayName = "Alert"; + +const AlertDescription = forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertDescription }; diff --git a/frontend/src/components/v2/Alert/index.tsx b/frontend/src/components/v2/Alert/index.tsx new file mode 100644 index 000000000..7851a0943 --- /dev/null +++ b/frontend/src/components/v2/Alert/index.tsx @@ -0,0 +1 @@ +export { Alert, AlertDescription } from "./Alert"; diff --git a/frontend/src/components/v2/index.tsx b/frontend/src/components/v2/index.tsx index 89030931d..31e2b4e57 100644 --- a/frontend/src/components/v2/index.tsx +++ b/frontend/src/components/v2/index.tsx @@ -1,4 +1,5 @@ export * from "./Accordion"; +export * from "./Alert"; export * from "./Button"; export * from "./Card"; export * from "./Checkbox"; diff --git a/frontend/src/hooks/api/apiKeys/index.ts b/frontend/src/hooks/api/apiKeys/index.ts new file mode 100644 index 000000000..c63762b97 --- /dev/null +++ b/frontend/src/hooks/api/apiKeys/index.ts @@ -0,0 +1,4 @@ +export { + useCreateAPIKeyV2, + useDeleteAPIKeyV2, + useUpdateAPIKeyV2} from "./queries"; \ No newline at end of file diff --git a/frontend/src/hooks/api/apiKeys/queries.tsx b/frontend/src/hooks/api/apiKeys/queries.tsx new file mode 100644 index 000000000..2d19146fe --- /dev/null +++ b/frontend/src/hooks/api/apiKeys/queries.tsx @@ -0,0 +1,62 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { userKeys } from "../users/queries"; +import { + APIKeyDataV2, + CreateAPIKeyDataV2DTO, + CreateServiceTokenDataV3Res, + DeleteAPIKeyDataV2DTO, + UpdateAPIKeyDataV2DTO} from "./types"; + +export const useCreateAPIKeyV2 = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + name + }) => { + const { data } = await apiRequest.post("/api/v3/api-key", { + name + }); + + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries(userKeys.myAPIKeysV2); + } + }); +}; + +export const useUpdateAPIKeyV2 = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + apiKeyDataId, + name + }) => { + const { data: { apiKeyData } } = await apiRequest.patch(`/api/v3/api-key/${apiKeyDataId}`, { + name + }); + return apiKeyData; + }, + onSuccess: () => { + queryClient.invalidateQueries(userKeys.myAPIKeysV2); + } + }); +}; + +export const useDeleteAPIKeyV2 = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + apiKeyDataId + }) => { + const { data: { apiKeyData } } = await apiRequest.delete(`/api/v3/api-key/${apiKeyDataId}`); + return apiKeyData; + }, + onSuccess: () => { + queryClient.invalidateQueries(userKeys.myAPIKeysV2); + } + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/apiKeys/types.ts b/frontend/src/hooks/api/apiKeys/types.ts new file mode 100644 index 000000000..9774c5824 --- /dev/null +++ b/frontend/src/hooks/api/apiKeys/types.ts @@ -0,0 +1,27 @@ +export type APIKeyDataV2 = { + _id: string; + name: string; + user: string; + lastUsed?: string; + usageCount: number; + createdAt: string; + updatedAt: string; + }; + +export type CreateAPIKeyDataV2DTO = { + name: string; +} + +export type CreateServiceTokenDataV3Res = { + apiKeyData: APIKeyDataV2; + apiKey: string; +} + +export type UpdateAPIKeyDataV2DTO = { + apiKeyDataId: string; + name: string; +} + +export type DeleteAPIKeyDataV2DTO = { + apiKeyDataId: string; +} \ No newline at end of file diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index b065b23a1..008f1e2fb 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -1,3 +1,4 @@ +export * from "./apiKeys"; export * from "./auditLogs"; export * from "./auth"; export * from "./bots"; diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx index ce6681911..a516589e5 100644 --- a/frontend/src/hooks/api/secrets/mutations.tsx +++ b/frontend/src/hooks/api/secrets/mutations.tsx @@ -131,6 +131,7 @@ export const useUpdateSecretV3 = ({ mutationFn: async ({ secretPath = "/", type, + secretId, environment, workspaceId, secretName, @@ -157,6 +158,7 @@ export const useUpdateSecretV3 = ({ environment, type, secretPath, + secretId, ...encryptSecret(randomBytes, newSecretName ?? secretName, secretValue, secretComment), tags, skipMultilineEncoding, diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index ceccf286c..b4c32d16d 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -109,6 +109,7 @@ export type TUpdateSecretsV3DTO = { skipMultilineEncoding?: boolean; newSecretName?: string; secretName: string; + secretId?: string; secretValue: string; secretComment?: string; tags?: string[]; diff --git a/frontend/src/hooks/api/serviceTokens/index.ts b/frontend/src/hooks/api/serviceTokens/index.ts index 2ae762611..01422d833 100644 --- a/frontend/src/hooks/api/serviceTokens/index.ts +++ b/frontend/src/hooks/api/serviceTokens/index.ts @@ -4,4 +4,5 @@ export { useDeleteServiceToken, useDeleteServiceTokenV3, useGetUserWsServiceTokens, - useUpdateServiceTokenV3} from "./queries"; + useUpdateServiceTokenV3 +} from "./queries"; diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index ce688fb0a..c04f7aa9f 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -7,6 +7,7 @@ export { useDeleteOrgMembership, useDeleteUser, useGetMyAPIKeys, + useGetMyAPIKeysV2, useGetMyIp, useGetMyOrganizationProjects, useGetMySessions, diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index adf0ccd09..45d8c323c 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -7,6 +7,7 @@ import { import { apiRequest } from "@app/config/request"; import { setAuthToken } from "@app/reactQuery"; +import { APIKeyDataV2 } from "../apiKeys/types"; import { useUploadWsKey } from "../keys/queries"; import { workspaceKeys } from "../workspace/queries"; import { @@ -24,12 +25,13 @@ import { User } from "./types"; -const userKeys = { +export const userKeys = { getUser: ["user"] as const, userAction: ["user-action"] as const, getOrgUsers: (orgId: string) => [{ orgId }, "user"], myIp: ["ip"] as const, myAPIKeys: ["api-keys"] as const, + myAPIKeysV2: ["api-keys-v2"] as const, mySessions: ["sessions"] as const, myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const }; @@ -270,7 +272,7 @@ export const useGetMyIp = () => { }); }; -export const useGetMyAPIKeys = () => { +export const useGetMyAPIKeys = () => { // TODO: deprecate (moving to API Key V2) return useQuery({ queryKey: userKeys.myAPIKeys, queryFn: async () => { @@ -281,7 +283,18 @@ export const useGetMyAPIKeys = () => { }); }; -export const useCreateAPIKey = () => { +export const useGetMyAPIKeysV2 = () => { + return useQuery({ + queryKey: userKeys.myAPIKeysV2, + queryFn: async () => { + const { data: { apiKeyData } } = await apiRequest.get<{ apiKeyData: APIKeyDataV2[] }>("/api/v3/users/me/api-keys"); + return apiKeyData; + }, + enabled: true + }); +}; + +export const useCreateAPIKey = () => { // TODO: deprecate (moving to API Key V2) const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ name, expiresIn }: { name: string; expiresIn: number }) => { @@ -298,7 +311,7 @@ export const useCreateAPIKey = () => { }); }; -export const useDeleteAPIKey = () => { +export const useDeleteAPIKey = () => { // TODO: deprecate (moving to API Key V2) const queryClient = useQueryClient(); return useMutation({ mutationFn: async (apiKeyDataId: string) => { diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index ac7be7d4c..04302548e 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -726,7 +726,7 @@ export const AppLayout = ({ children }: LayoutProps) => { {infisicalPlatformVersion && (
- Platform Version: {infisicalPlatformVersion} + Version: {infisicalPlatformVersion}
)}
diff --git a/frontend/src/pages/integrations/flyio/authorize.tsx b/frontend/src/pages/integrations/flyio/authorize.tsx index 277e62167..fe6d8022e 100644 --- a/frontend/src/pages/integrations/flyio/authorize.tsx +++ b/frontend/src/pages/integrations/flyio/authorize.tsx @@ -41,7 +41,6 @@ export default function FlyioAuthorizeIntegrationPage() { const onFormSubmit = async ({ accessToken }: FormData) => { - console.log("onFormSubmit accessToken: ", accessToken); try { setIsLoading(true); diff --git a/frontend/src/pages/integrations/hasura-cloud/authorize.tsx b/frontend/src/pages/integrations/hasura-cloud/authorize.tsx new file mode 100644 index 000000000..d2cff62ba --- /dev/null +++ b/frontend/src/pages/integrations/hasura-cloud/authorize.tsx @@ -0,0 +1,116 @@ +import { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; +import { useSaveIntegrationAccessToken } from "@app/hooks/api"; + +const schema = yup.object({ + accessToken: yup.string().trim().required("Hasura Cloud Access Token is required") +}); + +type FormData = yup.InferType; + +const APP_NAME = "Hasura Cloud"; +export default function HasuraCloudAuthorizeIntegrationPage() { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const { mutateAsync } = useSaveIntegrationAccessToken(); + + const { control, handleSubmit } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + accessToken: "" + } + }); + + const onFormSubmit = async ({ accessToken }: FormData) => { + try { + setIsLoading(true); + + const integrationAuth = await mutateAsync({ + workspaceId: localStorage.getItem("projectData.id"), + integration: "hasura-cloud", + accessToken + }); + + setIsLoading(false); + router.push(`/integrations/hasura-cloud/create?integrationAuthId=${integrationAuth._id}`); + } catch (err) { + setIsLoading(false); + console.error(err); + } + }; + return ( +
+ + Authorize {APP_NAME} Integration + + + + +
+
+ {`${APP_NAME} +
+ {APP_NAME} Integration + + +
+ + Docs + +
+
+ +
+
+
+ ( + + + + )} + /> + + +
+
+ ); +} + +HasuraCloudAuthorizeIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/hasura-cloud/create.tsx b/frontend/src/pages/integrations/hasura-cloud/create.tsx new file mode 100644 index 000000000..d104f6df7 --- /dev/null +++ b/frontend/src/pages/integrations/hasura-cloud/create.tsx @@ -0,0 +1,228 @@ +import { Controller, useForm } from "react-hook-form"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { yupResolver } from "@hookform/resolvers/yup"; +import queryString from "query-string"; +import * as yup from "yup"; + +import { + Button, + Card, + CardTitle, + FormControl, + Input, + Select, + SelectItem +} from "@app/components/v2"; +import { useCreateIntegration } from "@app/hooks/api"; +import { + useGetIntegrationAuthApps, + useGetIntegrationAuthById +} from "@app/hooks/api/integrationAuth"; +import { useGetWorkspaceById } from "@app/hooks/api/workspace"; + +const schema = yup.object({ + secretPath: yup.string().trim().required("Secret path is required"), + sourceEnvironment: yup.string().trim().required("Project environment is required"), + appId: yup.string().trim().required("Hasura Cloud project is required") +}); + +type FormData = yup.InferType; + +const APP_NAME = "Hasura Cloud"; +export default function HasuraCloudCreateIntegrationPage() { + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: yupResolver(schema) + }); + const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); + const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); + + const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); + const { data: integrationAuth, isLoading: isIntegrationAuthLoading } = useGetIntegrationAuthById( + (integrationAuthId as string) ?? "" + ); + + const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } = + useGetIntegrationAuthApps({ + integrationAuthId: (integrationAuthId as string) ?? "" + }); + + const onFormSubmit = async ({ secretPath, sourceEnvironment, appId }: FormData) => { + try { + if (!integrationAuth?._id) return; + + const app = integrationAuthApps?.find((data) => data.appId === appId); + await mutateAsync({ + integrationAuthId: integrationAuth?._id, + isActive: true, + sourceEnvironment, + secretPath, + appId, + app: app?.name + }); + + router.push(`/integrations/${localStorage.getItem("projectData.id")}`); + } catch (err) { + console.error(err); + } + }; + + return integrationAuth && workspace && integrationAuthApps ? ( +
+ + Set Up {APP_NAME} Integration + + + + +
+
+ {`${APP_NAME} +
+ {APP_NAME} Integration + + +
+ + Docs + +
+
+ +
+
+ +
+ ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + + +
+
+ ) : ( +
+ + Set Up {APP_NAME} Integration + + + {isIntegrationAuthLoading || isIntegrationAuthAppsLoading ? ( + infisical loading indicator + ) : ( +
+ +

+ Something went wrong. Please contact{" "} + + support@infisical.com + {" "} + if the issue persists. +

+
+ )} +
+ ); +} + +HasuraCloudCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx index c9fa6c585..5e92ec4ee 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx @@ -1,6 +1,6 @@ import crypto from "crypto"; -import { TCloudIntegration,UserWsKeyPair } from "@app/hooks/api/types"; +import { TCloudIntegration, UserWsKeyPair } from "@app/hooks/api/types"; import { decryptAssymmetric, @@ -32,11 +32,10 @@ export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) = export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => { try { - // generate CSRF token for OAuth2 code-token exchange integrations const state = crypto.randomBytes(16).toString("hex"); localStorage.setItem("latestCSRFToken", state); - + let link = ""; switch (integrationOption.slug) { case "gcp-secret-manager": @@ -123,6 +122,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => case "teamcity": link = `${window.location.origin}/integrations/teamcity/authorize`; break; + case "hasura-cloud": + link = `${window.location.origin}/integrations/hasura-cloud/authorize`; + break; default: break; } @@ -130,7 +132,6 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => if (link !== "") { window.location.assign(link); } - } catch (err) { console.error(err); } diff --git a/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx b/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx index fdc04af9d..fda0f6a1d 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx @@ -202,6 +202,8 @@ export const IntegrationsPage = withProjectPermission( integrations={integrations} environments={environments} onIntegrationDelete={({ _id: id }, cb) => handleIntegrationDelete(id, cb)} + isBotActive={bot?.isActive} + workspaceId={workspaceId} /> void) => void; + isBotActive: boolean | undefined; + workspaceId: string; }; export const IntegrationsSection = ({ integrations = [], environments = [], isLoading, - onIntegrationDelete + onIntegrationDelete, + isBotActive, + workspaceId }: Props) => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "deleteConfirmation" ] as const); + return (
@@ -45,7 +53,22 @@ export const IntegrationsSection = ({
)} - {!isLoading && !integrations.length && ( + + {!isBotActive && ( +
+ + + All the active integrations will be disabled. Disable End-to-End Encryption in{" "} + + project settings + + to re-enable it . + + +
+ )} + + {!isLoading && !integrations.length && isBotActive && (
)} - {!isLoading && ( + {!isLoading && isBotActive && (
{integrations?.map((integration) => (
{namespace}
- {filteredSecrets.map((secret) => ( { } }; - const handleSecretUpdate = async (env: string, key: string, value: string) => { + const handleSecretUpdate = async (env: string, key: string, value: string, secretId?: string) => { try { await updateSecretV3({ environment: env, workspaceId, secretPath, + secretId, secretName: key, secretValue: value, type: "shared", @@ -242,7 +243,6 @@ export const SecretOverviewPage = () => { ); const canViewOverviewPage = Boolean(userAvailableEnvs.length); - const filteredSecretNames = secKeys ?.filter((name) => name.toUpperCase().includes(searchFilter.toUpperCase())) .sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a))); diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx index 06a58172d..478d99086 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -18,7 +18,7 @@ type Props = { environment: string; secretPath: string; onSecretCreate: (env: string, key: string, value: string) => Promise; - onSecretUpdate: (env: string, key: string, value: string) => Promise; + onSecretUpdate: (env: string, key: string, value: string, secretId?: string) => Promise; onSecretDelete: (env: string, key: string, secretId?: string) => Promise; }; @@ -42,7 +42,7 @@ export const SecretEditRow = ({ formState: { isDirty, isSubmitting } } = useForm({ values: { - value: defaultValue + value: defaultValue || null } }); const [isDeleting, setIsDeleting] = useToggle(); @@ -70,7 +70,7 @@ export const SecretEditRow = ({ if (isCreatable) { await onSecretCreate(environment, secretName, value); } else { - await onSecretUpdate(environment, secretName, value); + await onSecretUpdate(environment, secretName, value, secretId); } } reset({ value }); @@ -80,7 +80,7 @@ export const SecretEditRow = ({ setIsDeleting.on(); try { await onSecretDelete(environment, secretName, secretId); - reset({ value: undefined }); + reset({ value: null }); } finally { setIsDeleting.off(); } diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx index 35a0e734f..8671d2da9 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx @@ -23,7 +23,7 @@ type Props = { expandableColWidth: number; getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined; onSecretCreate: (env: string, key: string, value: string) => Promise; - onSecretUpdate: (env: string, key: string, value: string) => Promise; + onSecretUpdate: (env: string, key: string, value: string, secretId?: string) => Promise; onSecretDelete: (env: string, key: string, secretId?: string) => Promise; }; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/SSOModal.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/SSOModal.tsx index a6655c546..813c1741d 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/SSOModal.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/SSOModal.tsx @@ -143,8 +143,8 @@ export const SSOModal = ({ entityId: "Identifier (Entity ID)", entryPoint: "Login URL", entryPointPlaceholder: "https://login.microsoftonline.com/xxx/saml2", - issuer: "Azure AD Identifier", - issuerPlaceholder: "https://sts.windows.net/xxx/" + issuer: "Azure Application ID", + issuerPlaceholder: "abc-def-ghi-jkl-mno" }); case AuthProvider.JUMPCLOUD_SAML: return ({ diff --git a/frontend/src/views/Settings/PersonalSettingsPage/APIKeyV2Section/APIKeyV2Modal.tsx b/frontend/src/views/Settings/PersonalSettingsPage/APIKeyV2Section/APIKeyV2Modal.tsx new file mode 100644 index 000000000..3cc010bee --- /dev/null +++ b/frontend/src/views/Settings/PersonalSettingsPage/APIKeyV2Section/APIKeyV2Modal.tsx @@ -0,0 +1,199 @@ +import { useEffect, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + Button, + FormControl, + IconButton, + Input, + Modal, + ModalContent} from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { + useCreateAPIKeyV2, + useUpdateAPIKeyV2 +} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = yup.object({ + name: yup.string().required("API Key V2 name is required") +}).required(); + +export type FormData = yup.InferType; + +type Props = { + popUp: UsePopUpState<["apiKeyV2"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["apiKeyV2"]>, state?: boolean) => void; +}; + +export const APIKeyV2Modal = ({ + popUp, + handlePopUpToggle +}: Props) => { + const [newAPIKey, setNewAPIKey] = useState(""); + const [isAPIKeyCopied, setIsAPIKeyCopied] = useToggle(false); + + const { createNotification } = useNotificationContext(); + + const { mutateAsync: createMutateAsync } = useCreateAPIKeyV2(); + const { mutateAsync: updateMutateAsync } = useUpdateAPIKeyV2(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + name: "" + } + }); + + useEffect(() => { + let timer: NodeJS.Timeout; + + if (isAPIKeyCopied) { + timer = setTimeout(() => setIsAPIKeyCopied.off(), 2000); + } + + return () => clearTimeout(timer); + }, [setIsAPIKeyCopied]); + + useEffect(() => { + const apiKeyData = popUp?.apiKeyV2?.data as { + apiKeyDataId: string; + name: string; + }; + + if (apiKeyData) { + reset({ + name: apiKeyData.name + }); + } else { + reset({ + name: "" + }); + } + }, [popUp?.apiKeyV2?.data]); + + const copyTokenToClipboard = () => { + navigator.clipboard.writeText(newAPIKey); + setIsAPIKeyCopied.on(); + }; + + const onFormSubmit = async ({ + name + }: FormData) => { + try { + const apiKeyData = popUp?.apiKeyV2?.data as { + apiKeyDataId: string; + name: string; + }; + + if (apiKeyData) { + // update + + await updateMutateAsync({ + apiKeyDataId: apiKeyData.apiKeyDataId, + name + }); + + handlePopUpToggle("apiKeyV2", false); + } else { + // create + + const { apiKey } = await createMutateAsync({ + name + }); + + setNewAPIKey(apiKey); + } + + createNotification({ + text: `Successfully ${popUp?.apiKeyV2?.data ? "updated" : "created"} API Key`, + type: "success" + }); + + reset(); + + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to ${popUp?.apiKeyV2?.data ? "updated" : "created"} API Key`, + type: "error" + }); + } + } + + const hasAPIKey = Boolean(newAPIKey); + + return ( + { + handlePopUpToggle("apiKeyV2", isOpen); + reset(); + setNewAPIKey(""); + }} + > + + {!hasAPIKey ? ( +
+ ( + + + + )} + /> +
+ + +
+ + ) : ( +
+

{newAPIKey}

+ + + + Click to copy + + +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/PersonalSettingsPage/APIKeyV2Section/APIKeyV2Section.tsx b/frontend/src/views/Settings/PersonalSettingsPage/APIKeyV2Section/APIKeyV2Section.tsx new file mode 100644 index 000000000..213d4bd65 --- /dev/null +++ b/frontend/src/views/Settings/PersonalSettingsPage/APIKeyV2Section/APIKeyV2Section.tsx @@ -0,0 +1,81 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + Button, + DeleteActionModal +} from "@app/components/v2"; +import { useDeleteAPIKeyV2 } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { APIKeyV2Modal } from "./APIKeyV2Modal"; +import { APIKeyV2Table } from "./APIKeyV2Table"; + +export const APIKeyV2Section = () => { + const { createNotification } = useNotificationContext(); + const { mutateAsync: deleteMutateAsync } = useDeleteAPIKeyV2(); + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "apiKeyV2", + "deleteAPIKeyV2" + ] as const); + + const onDeleteAPIKeyDataSubmit = async (apiKeyDataId: string) => { + try { + await deleteMutateAsync({ + apiKeyDataId + }); + + createNotification({ + text: "Successfully deleted API Key V2", + type: "success" + }); + + handlePopUpClose("deleteAPIKeyV2"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete API Key V2", + type: "error" + }); + } + } + + return ( +
+
+

+ API Keys V2 (Beta) +

+ +
+ + + handlePopUpToggle("deleteAPIKeyV2", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onDeleteAPIKeyDataSubmit( + (popUp?.deleteAPIKeyV2?.data as { apiKeyDataId: string })?.apiKeyDataId + ) + } + /> +
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/PersonalSettingsPage/APIKeyV2Section/APIKeyV2Table.tsx b/frontend/src/views/Settings/PersonalSettingsPage/APIKeyV2Section/APIKeyV2Table.tsx new file mode 100644 index 000000000..132723915 --- /dev/null +++ b/frontend/src/views/Settings/PersonalSettingsPage/APIKeyV2Section/APIKeyV2Table.tsx @@ -0,0 +1,107 @@ +import { faKey, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; + +import { + EmptyState, + IconButton, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { + useGetMyAPIKeysV2 +} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["deleteAPIKeyV2", "apiKeyV2"]>, + data?: { + apiKeyDataId?: string; + name?: string; + } + ) => void; + }; + +export const APIKeyV2Table = ({ + handlePopUpOpen +}: Props) => { + const { data, isLoading } = useGetMyAPIKeysV2(); + return ( + + + + + + + + + + + {isLoading && } + {!isLoading && + data && + data.length > 0 && + data.map(({ + _id, + name, + lastUsed, + createdAt + }) => { + return ( + + + + + + + ); + })} + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
NameLast UsedCreated At +
{name}{lastUsed ? format(new Date(lastUsed), "yyyy-MM-dd") : "-"}{format(new Date(createdAt), "yyyy-MM-dd")} + { + handlePopUpOpen("apiKeyV2", { + apiKeyDataId: _id, + name + }); + }} + size="lg" + colorSchema="primary" + variant="plain" + ariaLabel="update" + > + + + { + handlePopUpOpen("deleteAPIKeyV2", { + apiKeyDataId: _id + }); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="ml-4" + > + + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/PersonalSettingsPage/APIKeyV2Section/index.tsx b/frontend/src/views/Settings/PersonalSettingsPage/APIKeyV2Section/index.tsx new file mode 100644 index 000000000..b215d8028 --- /dev/null +++ b/frontend/src/views/Settings/PersonalSettingsPage/APIKeyV2Section/index.tsx @@ -0,0 +1 @@ +export { APIKeyV2Section } from "./APIKeyV2Section"; \ No newline at end of file diff --git a/frontend/src/views/Settings/PersonalSettingsPage/PersonalAPIKeyTab/PersonalAPIKeyTab.tsx b/frontend/src/views/Settings/PersonalSettingsPage/PersonalAPIKeyTab/PersonalAPIKeyTab.tsx index 24d40a8b1..c27e280a5 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/PersonalAPIKeyTab/PersonalAPIKeyTab.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/PersonalAPIKeyTab/PersonalAPIKeyTab.tsx @@ -1,7 +1,11 @@ +// import { APIKeyV2Section } from "../APIKeyV2Section"; import { APIKeySection } from "../APIKeySection"; export const PersonalAPIKeyTab = () => { return ( - + <> + {/* */} + + ); } \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx index 11e6128e7..2d62034d6 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/E2EESection/E2EESection.tsx @@ -3,7 +3,7 @@ import { decryptAssymmetric, encryptAssymmetric } from "@app/components/utilities/cryptography/crypto"; -import { Checkbox } from "@app/components/v2"; +import { Alert, AlertDescription, Checkbox } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api"; @@ -76,30 +76,39 @@ export const E2EESection = () => { }; return bot ? ( -
+

End-to-End Encryption

-

+

Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to cloud providers as well as HTTP calls to get secrets back raw but enables the server to read/decrypt your secret values.

-

+

Note that, even with E2EE disabled, your secrets are always encrypted at rest.

{(isAllowed) => ( -
- { - await toggleBotActivate(); - }} - > - End-to-end encryption enabled - +
+
+ { + await toggleBotActivate(); + }} + > + End-to-end encryption enabled + +
+
+ + + Enabling End-to-end encryption disables all the integrations + + +
)} diff --git a/helm-charts/infisical/Chart.yaml b/helm-charts/infisical/Chart.yaml index 97fca592e..3be4eeb27 100644 --- a/helm-charts/infisical/Chart.yaml +++ b/helm-charts/infisical/Chart.yaml @@ -7,7 +7,7 @@ type: application # 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. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.3.5 +version: 0.4.1 # 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 diff --git a/helm-charts/infisical/README.md b/helm-charts/infisical/README.md index 4aca077a9..46a219a66 100644 --- a/helm-charts/infisical/README.md +++ b/helm-charts/infisical/README.md @@ -4,7 +4,6 @@ This is the Infisical application Helm chart. This chart includes the following | Service | Description | | ---------- | ----------------------------------- | -| `frontend` | Infisical's Web UI | | `backend` | Infisical's API | | `mongodb` | Infisical's database | | `redis` | Infisical's cache service | @@ -59,28 +58,6 @@ kubectl get secrets -n \ | `nameOverride` | Override release name | `""` | | `fullnameOverride` | Override release fullname | `""` | -### Infisical frontend parameters - -| Name | Description | Value | -| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -| `frontend.enabled` | Enable frontend | `true` | -| `frontend.name` | Frontend name | `frontend` | -| `frontend.fullnameOverride` | Frontend fullnameOverride | `""` | -| `frontend.podAnnotations` | Frontend pod annotations | `{}` | -| `frontend.deploymentAnnotations` | Frontend deployment annotations | `{}` | -| `frontend.replicaCount` | Frontend replica count | `2` | -| `frontend.image.repository` | Frontend image repository | `infisical/frontend` | -| `frontend.image.tag` | Frontend image tag | `latest` | -| `frontend.image.pullPolicy` | Frontend image pullPolicy | `IfNotPresent` | -| `frontend.resources.limits.memory` | container memory limit [check the offical kubernetes documentations](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) | `100Mi` | -| `frontend.resources.requests.cpu` | container CPU request [check the offical kubernetes documentations](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) | `100m` | -| `frontend.affinity` | Frontend pod affinity | `{}` | -| `frontend.kubeSecretRef` | Backend secret resource reference name (containing required [frontend configuration variables](https://infisical.com/docs/self-hosting/configuration/envars)) | `""` | -| `frontend.service.annotations` | Backend service annotations | `{}` | -| `frontend.service.type` | Backend service type | `ClusterIP` | -| `frontend.service.nodePort` | Backend service nodePort (used if above type is `NodePort`) | `""` | -| `frontendEnvironmentVariables.SITE_URL` | Absolute URL including the protocol (e.g. https://app.infisical.com) | `infisical.local` | - ### Infisical backend parameters | Name | Description | Value | @@ -91,11 +68,9 @@ kubectl get secrets -n \ | `backend.podAnnotations` | Backend pod annotations | `{}` | | `backend.deploymentAnnotations` | Backend deployment annotations | `{}` | | `backend.replicaCount` | Backend replica count | `2` | -| `backend.image.repository` | Backend image repository | `infisical/backend` | +| `backend.image.repository` | Backend image repository | `infisical/infisical` | | `backend.image.tag` | Backend image tag | `latest` | | `backend.image.pullPolicy` | Backend image pullPolicy | `IfNotPresent` | -| `backend.resources.limits.memory` | container memory limit [check the offical kubernetes documentations](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) | `200Mi` | -| `backend.resources.requests.cpu` | container CPU request [check the offical kubernetes documentations](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) | `150m` | | `backend.affinity` | Backend pod affinity | `{}` | | `backend.kubeSecretRef` | Backend secret resource reference name (containing required [backend configuration variables](https://infisical.com/docs/self-hosting/configuration/envars)) | `""` | | `backend.service.annotations` | Backend service annotations | `{}` | @@ -197,6 +172,7 @@ kubectl get secrets -n \ + ## Persistence The database persistence is enabled by default, your volumes will remain on your cluster even after uninstalling the chart. To disable persistence, set this value `mongodb.persistence.enabled: false` diff --git a/helm-charts/infisical/templates/NOTES.txt b/helm-charts/infisical/templates/NOTES.txt index 103ec9a5b..a5f3cd005 100644 --- a/helm-charts/infisical/templates/NOTES.txt +++ b/helm-charts/infisical/templates/NOTES.txt @@ -2,34 +2,6 @@ -- Infisical Helm Chart -- - __ __ - ( _) ( _) - / / \\ / /\_\_ - / / \\ / / | \ \ - / / \\ / / |\ \ \ - / / , \ , / / /| \ \ - / / |\_ /| / / / \ \_\ - / / |\/ _ '_|\ / / / \ \\ - | / |/ 0 \0\\ / | | \ \\ - | |\| \_\_ / / | \ \\ - | | |/ \.\ o\o) / \ | \\ - \ | /\\`v-v / | | \\ - | \/ /_| \\_| / | | \ \\ - | | /__/_ / _____ | | \ \\ - \| [__] \_/ |_________ \ | \ () - / [___] ( \ \ |\ | | // - | [___] |\| \| / |/ - /| [____] \ |/\ / / || - ( \ [____ / ) _\ \ \ \| | || - \ \ [_____| / / __/ \ / / // - | \ [_____/ / / \ | \/ // - | / '----| /=\____ _/ | / // - __ / / | / ___/ _/\ \ | || - (/-(/-\) / \ (/\/\)/ | / | / - (/\/\) / / // - _________/ / / - \____________/ ( - โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ @@ -46,7 +18,6 @@ โ”‚ Visit < https://infisical.com/docs/self-hosting/overview > for further documentation about self-hosting! โ”‚ โ”‚ Current installation (infisical) : -โ”‚ โ€ข infisical-frontend : {{ .Values.frontend.enabled }} โ”‚ โ€ข infisical-backend : {{ .Values.backend.enabled }} โ”‚ โ€ข mongodb : {{ .Values.mongodb.enabled }} โ”‚ โ€ข mailhog : {{ .Values.mailhog.enabled }} diff --git a/helm-charts/infisical/templates/_helpers.tpl b/helm-charts/infisical/templates/_helpers.tpl index 500edea33..40180896f 100644 --- a/helm-charts/infisical/templates/_helpers.tpl +++ b/helm-charts/infisical/templates/_helpers.tpl @@ -41,16 +41,6 @@ component: {{ .Values.backend.name | quote }} {{ include "infisical.common.matchLabels" . }} {{- end -}} -{{- define "infisical.frontend.labels" -}} -{{ include "infisical.frontend.matchLabels" . }} -{{ include "infisical.common.metaLabels" . }} -{{- end -}} - -{{- define "infisical.frontend.matchLabels" -}} -component: {{ .Values.frontend.name | quote }} -{{ include "infisical.common.matchLabels" . }} -{{- end -}} - {{- define "infisical.mongodb.labels" -}} {{ include "infisical.mongodb.matchLabels" . }} {{ include "infisical.common.metaLabels" . }} @@ -78,22 +68,6 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this {{- end -}} {{- end -}} -{{/* -Create a fully qualified frontend name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -*/}} -{{- define "infisical.frontend.fullname" -}} -{{- if .Values.frontend.fullnameOverride -}} -{{- .Values.frontend.fullnameOverride | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- if contains $name .Release.Name -}} -{{- printf "%s-%s" .Release.Name .Values.frontend.name | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- printf "%s-%s-%s" .Release.Name $name .Values.frontend.name | trunc 63 | trimSuffix "-" -}} -{{- end -}} -{{- end -}} -{{- end -}} {{/* Create a fully qualified mongodb name. diff --git a/helm-charts/infisical/templates/backend-deployment.yaml b/helm-charts/infisical/templates/backend-deployment.yaml index 86c00c1bc..dced5f0a2 100644 --- a/helm-charts/infisical/templates/backend-deployment.yaml +++ b/helm-charts/infisical/templates/backend-deployment.yaml @@ -36,17 +36,17 @@ spec: readinessProbe: httpGet: path: /api/status - port: 4000 + port: 8080 initialDelaySeconds: 10 periodSeconds: 10 ports: - - containerPort: 4000 + - containerPort: 8080 envFrom: - secretRef: name: {{ $backend.kubeSecretRef | default (include "infisical.backend.fullname" .) }} - {{- if $backend.resources }} - resources: {{- toYaml $backend.resources | nindent 12 }} - {{- end }} + # {{- if $backend.resources }} + # resources: {{- toYaml $backend.resources | nindent 12 }} + # {{- end }} --- apiVersion: v1 @@ -65,8 +65,8 @@ spec: {{- include "infisical.backend.matchLabels" . | nindent 8 }} ports: - protocol: TCP - port: 4000 - targetPort: 4000 # container port + port: 8080 + targetPort: 8080 # container port {{- if eq $backend.service.type "NodePort" }} nodePort: {{ $backend.service.nodePort }} {{- end }} diff --git a/helm-charts/infisical/templates/frontend-deployment.yaml b/helm-charts/infisical/templates/frontend-deployment.yaml deleted file mode 100644 index 043a94263..000000000 --- a/helm-charts/infisical/templates/frontend-deployment.yaml +++ /dev/null @@ -1,94 +0,0 @@ -{{- $frontend := .Values.frontend }} -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "infisical.frontend.fullname" . }} - annotations: - updatedAt: {{ now | date "2006-01-01 MST 15:04:05" | quote }} - {{- with .Values.frontend.deploymentAnnotations }} - {{- toYaml . | nindent 4 }} - {{- end }} - labels: - {{- include "infisical.frontend.labels" . | nindent 4 }} -spec: - replicas: {{ $frontend.replicaCount }} - selector: - matchLabels: - {{- include "infisical.frontend.matchLabels" . | nindent 6 }} - template: - metadata: - labels: - {{- include "infisical.frontend.matchLabels" . | nindent 8 }} - annotations: - updatedAt: {{ now | date "2006-01-01 MST 15:04:05" | quote }} - {{- with $frontend.podAnnotations }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with $frontend.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: {{ template "infisical.name" . }}-{{ $frontend.name }} - image: "{{ $frontend.image.repository }}:{{ $frontend.image.tag | default "latest" }}" - imagePullPolicy: {{ $frontend.image.pullPolicy }} - readinessProbe: - httpGet: - path: / - port: 3000 - initialDelaySeconds: 10 - periodSeconds: 10 - envFrom: - - secretRef: - name: {{ $frontend.kubeSecretRef | default (include "infisical.frontend.fullname" .) }} - ports: - - containerPort: 3000 - {{- if $frontend.resources }} - resources: {{- toYaml $frontend.resources | nindent 12 }} - {{- end }} ---- - -apiVersion: v1 -kind: Service -metadata: - name: {{ include "infisical.frontend.fullname" . }} - labels: - {{- include "infisical.frontend.labels" . | nindent 4 }} - {{- with $frontend.service.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - type: {{ $frontend.service.type }} - selector: - {{- include "infisical.frontend.matchLabels" . | nindent 8 }} - ports: - - protocol: TCP - port: 3000 # service - targetPort: 3000 # container port - {{- if eq $frontend.service.type "NodePort" }} - nodePort: {{ $frontend.service.nodePort }} - {{- end }} - ---- - -{{ if not $frontend.kubeSecretRef }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "infisical.frontend.fullname" . }} - annotations: - "helm.sh/resource-policy": "keep" -type: Opaque -stringData: - {{- $requiredVars := dict }} - {{- $secretObj := (lookup "v1" "Secret" .Release.Namespace (include "infisical.frontend.fullname" .)) | default dict }} - {{- $secretData := (get $secretObj "data") | default dict }} - {{ range $key, $value := .Values.frontendEnvironmentVariables }} - {{- $default := get $requiredVars $key -}} - {{- $current := get $secretData $key | b64dec -}} - {{- $v := $value | default ($current | default $default) -}} - {{ $key }}: {{ $v | quote }} - {{ end -}} -{{- end }} \ No newline at end of file diff --git a/helm-charts/infisical/templates/ingress.yaml b/helm-charts/infisical/templates/ingress.yaml index 659948510..ab5253ca6 100644 --- a/helm-charts/infisical/templates/ingress.yaml +++ b/helm-charts/infisical/templates/ingress.yaml @@ -30,27 +30,20 @@ spec: rules: - http: paths: - - path: {{ $ingress.frontend.path }} - pathType: {{ $ingress.frontend.pathType }} - backend: - service: - name: {{ include "infisical.frontend.fullname" . }} - port: - number: 3000 - - path: {{ $ingress.backend.path }} - pathType: {{ $ingress.backend.pathType }} + - path: / + pathType: Prefix backend: service: name: {{ include "infisical.backend.fullname" . }} port: - number: 4000 + number: 8080 - path: /ss-webhook pathType: Exact backend: service: name: {{ include "infisical.backend.fullname" . }} port: - number: 4000 + number: 8080 {{- if $ingress.hostName }} host: {{ $ingress.hostName }} {{- end }} diff --git a/helm-charts/infisical/values.yaml b/helm-charts/infisical/values.yaml index 12706c56b..70fca908d 100644 --- a/helm-charts/infisical/values.yaml +++ b/helm-charts/infisical/values.yaml @@ -8,76 +8,6 @@ nameOverride: "" ## fullnameOverride: "" -## @section Infisical frontend parameters -## Documentation : https://infisical.com/docs/self-hosting/deployments/kubernetes -## - -frontend: - ## @param frontend.enabled Enable frontend - ## - enabled: true - ## @param frontend.name Frontend name - ## - name: frontend - ## @param frontend.fullnameOverride Frontend fullnameOverride - ## - fullnameOverride: "" - ## @param frontend.podAnnotations Frontend pod annotations - ## - podAnnotations: {} - ## @param frontend.deploymentAnnotations Frontend deployment annotations - ## - deploymentAnnotations: {} - ## @param frontend.replicaCount Frontend replica count - ## - replicaCount: 2 - ## Frontend image parameters - ## - image: - ## @param frontend.image.repository Frontend image repository - ## - repository: infisical/frontend - ## @param frontend.image.tag Frontend image tag - ## - tag: "latest" - ## @param frontend.image.pullPolicy Frontend image pullPolicy - ## - pullPolicy: IfNotPresent - ## @param frontend.resources.limits.memory container memory limit [check the offical kubernetes documentations](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) - ## @param frontend.resources.requests.cpu container CPU request [check the offical kubernetes documentations](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) - ## - resources: - limits: - memory: 100Mi - requests: - cpu: 100m - ## @param frontend.affinity Frontend pod affinity - ## - affinity: {} - ## @param frontend.kubeSecretRef Backend secret resource reference name (containing required [frontend configuration variables](https://infisical.com/docs/self-hosting/configuration/envars)) - ## - kubeSecretRef: "" - ## Frontend service - ## - service: - ## @param frontend.service.annotations Backend service annotations - ## - annotations: {} - ## @param frontend.service.type Backend service type - ## - type: ClusterIP - ## @param frontend.service.nodePort Backend service nodePort (used if above type is `NodePort`) - ## - nodePort: "" - -## Frontend variables configuration -## Documentation : https://infisical.com/docs/self-hosting/configuration/envars -## -frontendEnvironmentVariables: - ## @param frontendEnvironmentVariables.SITE_URL Absolute URL including the protocol (e.g. https://app.infisical.com) - ## - SITE_URL: infisical.local - ## @section Infisical backend parameters ## Documentation : https://infisical.com/docs/self-hosting/deployments/kubernetes ## @@ -106,21 +36,13 @@ backend: image: ## @param backend.image.repository Backend image repository ## - repository: infisical/backend + repository: infisical/infisical ## @param backend.image.tag Backend image tag ## tag: "latest" ## @param backend.image.pullPolicy Backend image pullPolicy ## pullPolicy: IfNotPresent - ## @param backend.resources.limits.memory container memory limit [check the offical kubernetes documentations](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) - ## @param backend.resources.requests.cpu container CPU request [check the offical kubernetes documentations](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) - ## - resources: - limits: - memory: 200Mi - requests: - cpu: 150m ## @param backend.affinity Backend pod affinity ## affinity: {} @@ -349,16 +271,6 @@ ingress: ## Replace with your own domain ## hostName: "" - ## @skip ingress.frontend - ## - frontend: - path: / - pathType: Prefix - ## @skip ingress.backend - ## - backend: - path: /api - pathType: Prefix ## @param ingress.tls Ingress TLS hosts (matching above hostName) ## Replace with your own domain ## diff --git a/nginx/default-stand-alone-docker.conf b/nginx/default-stand-alone-docker.conf deleted file mode 100644 index b40e0fb13..000000000 --- a/nginx/default-stand-alone-docker.conf +++ /dev/null @@ -1,36 +0,0 @@ -events {} -http { - server { - listen 80; - - location /api { - proxy_set_header X-Real-RIP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_set_header Host $http_host; - proxy_set_header X-NginX-Proxy true; - - proxy_pass http://localhost:4000; # for backend - proxy_redirect off; - - # proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; - proxy_cookie_path / "/; HttpOnly; SameSite=strict"; - } - - location / { - include /etc/nginx/mime.types; - - proxy_set_header X-Real-RIP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_set_header Host $http_host; - proxy_set_header X-NginX-Proxy true; - - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - proxy_pass http://localhost:3000; # for frontend - proxy_redirect off; - } - } -} \ No newline at end of file diff --git a/standalone-entrypoint.sh b/standalone-entrypoint.sh new file mode 100755 index 000000000..e15931dc4 --- /dev/null +++ b/standalone-entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +cd frontend-build +scripts/initialize-standalone-build.sh + +cd ../ + +exec node build/index.js