diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml index 3d11a1157..af395ce6c 100644 --- a/.github/workflows/release_build.yml +++ b/.github/workflows/release_build.yml @@ -4,7 +4,7 @@ on: push: # run only against tags tags: - - 'v*' + - "v*" permissions: contents: write @@ -18,11 +18,16 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - run: git fetch --force --tags - run: echo "Ref name ${{github.ref_name}}" - uses: actions/setup-go@v3 with: - go-version: '>=1.19.3' + go-version: ">=1.19.3" cache: true cache-dependency-path: cli/go.sum - name: libssl1.1 => libssl1.0-dev for OSXCross @@ -45,8 +50,7 @@ jobs: AUR_KEY: ${{ secrets.AUR_KEY }} - uses: actions/setup-python@v4 - run: pip install --upgrade cloudsmith-cli - - name: Publish to CloudSmith + - name: Publish to CloudSmith run: sh cli/upload_to_cloudsmith.sh env: CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} - diff --git a/.goreleaser.yaml b/.goreleaser.yaml index fc39224aa..dfcacf59d 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -68,10 +68,10 @@ archives: release: replace_existing_draft: true - mode: 'replace' + mode: "replace" checksum: - name_template: 'checksums.txt' + name_template: "checksums.txt" snapshot: name_template: "{{ incpatch .Version }}-devel" @@ -80,8 +80,8 @@ changelog: sort: asc filters: exclude: - - '^docs:' - - '^test:' + - "^docs:" + - "^test:" # publishers: # - name: fury.io @@ -109,30 +109,30 @@ brews: man1.install "manpages/infisical.1.gz" nfpms: -- id: infisical - package_name: infisical - builds: - - all-other-builds - vendor: Infisical, Inc - homepage: https://infisical.com/ - maintainer: Infisical, Inc - description: The offical Infisical CLI - license: MIT - formats: - - rpm - - deb - - apk - - archlinux - bindir: /usr/bin - contents: - - src: ./completions/infisical.bash - dst: /etc/bash_completion.d/infisical - - src: ./completions/infisical.fish - dst: /usr/share/fish/vendor_completions.d/infisical.fish - - src: ./completions/infisical.zsh - dst: /usr/share/zsh/site-functions/_infisical - - src: ./manpages/infisical.1.gz - dst: /usr/share/man/man1/infisical.1.gz + - id: infisical + package_name: infisical + builds: + - all-other-builds + vendor: Infisical, Inc + homepage: https://infisical.com/ + maintainer: Infisical, Inc + description: The offical Infisical CLI + license: MIT + formats: + - rpm + - deb + - apk + - archlinux + bindir: /usr/bin + contents: + - src: ./completions/infisical.bash + dst: /etc/bash_completion.d/infisical + - src: ./completions/infisical.fish + dst: /usr/share/fish/vendor_completions.d/infisical.fish + - src: ./completions/infisical.zsh + dst: /usr/share/zsh/site-functions/_infisical + - src: ./manpages/infisical.1.gz + dst: /usr/share/man/man1/infisical.1.gz scoop: bucket: @@ -146,15 +146,14 @@ scoop: license: MIT aurs: - - - name: infisical-bin + - name: infisical-bin homepage: "https://infisical.com" description: "The official Infisical CLI" maintainers: - Infisical, Inc license: MIT - private_key: '{{ .Env.AUR_KEY }}' - git_url: 'ssh://aur@aur.archlinux.org/infisical-bin.git' + private_key: "{{ .Env.AUR_KEY }}" + git_url: "ssh://aur@aur.archlinux.org/infisical-bin.git" package: |- # bin install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical" @@ -169,19 +168,13 @@ aurs: install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish" # man pages install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz" + # dockers: -# - dockerfile: goreleaser.dockerfile +# - dockerfile: cli/docker/Dockerfile # goos: linux # goarch: amd64 # ids: # - infisical # image_templates: -# - "infisical/cli:{{ .Version }}" -# - "infisical/cli:{{ .Major }}.{{ .Minor }}" -# - "infisical/cli:{{ .Major }}" +# - "infisical/cli:{{ .Version }}" # - "infisical/cli:latest" -# build_flag_templates: -# - "--label=org.label-schema.schema-version=1.0" -# - "--label=org.label-schema.version={{.Version}}" -# - "--label=org.label-schema.name={{.ProjectName}}" -# - "--platform=linux/amd64" \ No newline at end of file diff --git a/README.md b/README.md index 64d643e7f..0331567ec 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,7 @@ Dashboard - -**Read this in other languages**: [English language](i18n/README.en.md) +**Read this in other languages**: [English language](i18n/README.en.md) [Spanish language](i18n/README.es.md) [Korean language](i18n/README.ko.md) [Turkish language](i18n/README.tr.md) @@ -358,7 +357,7 @@ Infisical officially launched as v.1.0 on November 21st, 2022. There are a lot o - + ## 🌎 Translations diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index a7194308e..1d8665c72 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,5 +1,6 @@ const PORT = process.env.PORT || 4000; const EMAIL_TOKEN_LIFETIME = parseInt(process.env.EMAIL_TOKEN_LIFETIME! || '86400'); +const INVITE_ONLY_SIGNUP = process.env.INVITE_ONLY_SIGNUP == undefined ? false : process.env.INVITE_ONLY_SIGNUP const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10; const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; @@ -50,6 +51,7 @@ const LICENSE_KEY = process.env.LICENSE_KEY!; export { PORT, EMAIL_TOKEN_LIFETIME, + INVITE_ONLY_SIGNUP, ENCRYPTION_KEY, SALT_ROUNDS, JWT_AUTH_LIFETIME, diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index 4150890ac..11cbd6f64 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -35,14 +35,11 @@ export const getIntegrationAuth = async (req: Request, res: Response) => { }); } -export const getIntegrationOptions = async ( - req: Request, - res: Response -) => { - return res.status(200).send({ - integrationOptions: INTEGRATION_OPTIONS - }); -} +export const getIntegrationOptions = async (req: Request, res: Response) => { + return res.status(200).send({ + integrationOptions: INTEGRATION_OPTIONS, + }); +}; /** * Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId] @@ -90,8 +87,8 @@ export const oAuthExchange = async ( * @param res */ export const saveIntegrationAccessToken = async ( - req: Request, - res: Response + req: Request, + res: Response ) => { // TODO: refactor // TODO: check if access token is valid for each integration @@ -157,23 +154,23 @@ export const saveIntegrationAccessToken = async ( * @returns */ export const getIntegrationAuthApps = async (req: Request, res: Response) => { - let apps; - try { - apps = await getApps({ - integrationAuth: req.integrationAuth, - accessToken: req.accessToken - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get integration authorization applications' - }); - } + let apps; + try { + apps = await getApps({ + integrationAuth: req.integrationAuth, + accessToken: req.accessToken, + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to get integration authorization applications", + }); + } - return res.status(200).send({ - apps - }); + return res.status(200).send({ + apps, + }); }; /** @@ -183,21 +180,21 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => { * @returns */ export const deleteIntegrationAuth = async (req: Request, res: Response) => { - let integrationAuth; - try { - integrationAuth = await revokeAccess({ - integrationAuth: req.integrationAuth, - accessToken: req.accessToken - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete integration authorization' - }); - } - - return res.status(200).send({ - integrationAuth - }); -} \ No newline at end of file + let integrationAuth; + try { + integrationAuth = await revokeAccess({ + integrationAuth: req.integrationAuth, + accessToken: req.accessToken, + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to delete integration authorization", + }); + } + + return res.status(200).send({ + integrationAuth, + }); +}; diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 621040aee..a17e4d7a6 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -12,9 +12,9 @@ import { eventPushSecrets } from '../../events'; /** * Create/initialize an (empty) integration for integration authorization - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const createIntegration = async (req: Request, res: Response) => { let integration; @@ -65,10 +65,10 @@ export const createIntegration = async (req: Request, res: Response) => { }); } - return res.status(200).send({ - integration - }); -} + return res.status(200).send({ + integration, + }); +}; /** * Change environment or name of integration with id [integrationId] @@ -77,57 +77,57 @@ export const createIntegration = async (req: Request, res: Response) => { * @returns */ export const updateIntegration = async (req: Request, res: Response) => { - let integration; - - // TODO: add integration-specific validation to ensure that each - // integration has the correct fields populated in [Integration] - - try { - const { - environment, - isActive, - app, - appId, - targetEnvironment, - owner, // github-specific integration param - } = req.body; - - integration = await Integration.findOneAndUpdate( - { - _id: req.integration._id - }, - { - environment, - isActive, - app, - appId, - targetEnvironment, - owner - }, - { - new: true - } - ); - - if (integration) { - // trigger event - push secrets - EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: integration.workspace.toString() - }) - }); - } - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to update integration' - }); - } + let integration; - return res.status(200).send({ - integration - }); + // TODO: add integration-specific validation to ensure that each + // integration has the correct fields populated in [Integration] + + try { + const { + environment, + isActive, + app, + appId, + targetEnvironment, + owner, // github-specific integration param + } = req.body; + + integration = await Integration.findOneAndUpdate( + { + _id: req.integration._id, + }, + { + environment, + isActive, + app, + appId, + targetEnvironment, + owner, + }, + { + new: true, + } + ); + + if (integration) { + // trigger event - push secrets + EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: integration.workspace.toString(), + }), + }); + } + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to update integration", + }); + } + + return res.status(200).send({ + integration, + }); }; /** @@ -138,24 +138,24 @@ export const updateIntegration = async (req: Request, res: Response) => { * @returns */ export const deleteIntegration = async (req: Request, res: Response) => { - let integration; - try { - const { integrationId } = req.params; + let integration; + try { + const { integrationId } = req.params; - integration = await Integration.findOneAndDelete({ - _id: integrationId - }); - - if (!integration) throw new Error('Failed to find integration'); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete integration' - }); - } - - return res.status(200).send({ - integration - }); + integration = await Integration.findOneAndDelete({ + _id: integrationId, + }); + + if (!integration) throw new Error("Failed to find integration"); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to delete integration", + }); + } + + return res.status(200).send({ + integration, + }); }; diff --git a/backend/src/controllers/v1/organizationController.ts b/backend/src/controllers/v1/organizationController.ts index eaf58fad7..66326e560 100644 --- a/backend/src/controllers/v1/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -397,9 +397,21 @@ export const getOrganizationMembersAndTheirWorkspaces = async ( res: Response ) => { const { organizationId } = req.params; - const orgMemberships = await MembershipOrg.find({ organization: organizationId }); - const userIds = orgMemberships.map(orgMembership => orgMembership.user); - const memberships = await Membership.find({ user: { $in: userIds } }); + + const workspacesSet = ( + await Workspace.find( + { + organization: organizationId + }, + '_id' + ) + ).map((w) => w._id.toString()); + + const memberships = ( + await Membership.find({ + workspace: { $in: workspacesSet } + }).populate('workspace') + ); const userToWorkspaceIds: any = {}; memberships.forEach(membership => { @@ -411,15 +423,5 @@ export const getOrganizationMembersAndTheirWorkspaces = async ( } }); - const workspaceIds = Object.values(userToWorkspaceIds).flat() - const workspacesList = await Workspace.find({ - organization: organizationId, - _id: { $in: workspaceIds } - }); - - const populatedUserWorkspaces = _.mapValues(userToWorkspaceIds, workspaceIds => - _.map(workspaceIds, id => _.find(workspacesList, { _id: id })) - ); - - return res.json(populatedUserWorkspaces); + return res.json(userToWorkspaceIds); }; \ No newline at end of file diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index 62e5a62a3..dad9632db 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config'; +import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, INVITE_ONLY_SIGNUP } from '../../config'; import { User, MembershipOrg } from '../../models'; import { completeAccount } from '../../helpers/user'; import { @@ -11,6 +11,7 @@ import { import { issueTokens, createToken } from '../../helpers/auth'; import { INVITED, ACCEPTED } from '../../variables'; import axios from 'axios'; +import { BadRequestError } from '../../utils/errors'; /** * Signup step 1: Initialize account for user under email [email] and send a verification code @@ -24,6 +25,14 @@ export const beginEmailSignup = async (req: Request, res: Response) => { try { email = req.body.email; + if (INVITE_ONLY_SIGNUP) { + // Only one user can create an account without being invited. The rest need to be invited in order to make an account + const userCount = await User.countDocuments({}) + if (userCount != 0) { + throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." }) + } + } + const user = await User.findOne({ email }).select('+publicKey'); if (user && user?.publicKey) { // case: user has already completed account @@ -129,7 +138,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { // get user user = await User.findOne({ email }); - + if (!user || (user && user?.publicKey)) { // case 1: user doesn't exist. // case 2: user has already completed account diff --git a/backend/src/controllers/v1/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts index 8675fc2b6..2b0a89f43 100644 --- a/backend/src/controllers/v1/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -1,21 +1,21 @@ -import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; +import { Request, Response } from "express"; +import * as Sentry from "@sentry/node"; import { - Workspace, - Membership, - MembershipOrg, - Integration, - IntegrationAuth, - IUser, - ServiceToken, - ServiceTokenData -} from '../../models'; + Workspace, + Membership, + MembershipOrg, + Integration, + IntegrationAuth, + IUser, + ServiceToken, + ServiceTokenData, +} from "../../models"; import { - createWorkspace as create, - deleteWorkspace as deleteWork -} from '../../helpers/workspace'; -import { addMemberships } from '../../helpers/membership'; -import { ADMIN } from '../../variables'; + createWorkspace as create, + deleteWorkspace as deleteWork, +} from "../../helpers/workspace"; +import { addMemberships } from "../../helpers/membership"; +import { ADMIN } from "../../variables"; /** * Return public keys of members of workspace with id [workspaceId] @@ -24,32 +24,31 @@ import { ADMIN } from '../../variables'; * @returns */ export const getWorkspacePublicKeys = async (req: Request, res: Response) => { - let publicKeys; - try { - const { workspaceId } = req.params; + let publicKeys; + try { + const { workspaceId } = req.params; - publicKeys = ( - await Membership.find({ - workspace: workspaceId - }).populate<{ user: IUser }>('user', 'publicKey') - ) - .map((member) => { - return { - publicKey: member.user.publicKey, - userId: member.user._id - }; - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace member public keys' - }); - } + publicKeys = ( + await Membership.find({ + workspace: workspaceId, + }).populate<{ user: IUser }>("user", "publicKey") + ).map((member) => { + return { + publicKey: member.user.publicKey, + userId: member.user._id, + }; + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to get workspace member public keys", + }); + } - return res.status(200).send({ - publicKeys - }); + return res.status(200).send({ + publicKeys, + }); }; /** @@ -59,24 +58,24 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => { * @returns */ export const getWorkspaceMemberships = async (req: Request, res: Response) => { - let users; - try { - const { workspaceId } = req.params; + let users; + try { + const { workspaceId } = req.params; - users = await Membership.find({ - workspace: workspaceId - }).populate('user', '+publicKey'); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace members' - }); - } + users = await Membership.find({ + workspace: workspaceId, + }).populate("user", "+publicKey"); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to get workspace members", + }); + } - return res.status(200).send({ - users - }); + return res.status(200).send({ + users, + }); }; /** @@ -86,24 +85,24 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => { * @returns */ export const getWorkspaces = async (req: Request, res: Response) => { - let workspaces; - try { - workspaces = ( - await Membership.find({ - user: req.user._id - }).populate('workspace') - ).map((m) => m.workspace); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspaces' - }); - } + let workspaces; + try { + workspaces = ( + await Membership.find({ + user: req.user._id, + }).populate("workspace") + ).map((m) => m.workspace); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to get workspaces", + }); + } - return res.status(200).send({ - workspaces - }); + return res.status(200).send({ + workspaces, + }); }; /** @@ -113,24 +112,24 @@ export const getWorkspaces = async (req: Request, res: Response) => { * @returns */ export const getWorkspace = async (req: Request, res: Response) => { - let workspace; - try { - const { workspaceId } = req.params; + let workspace; + try { + const { workspaceId } = req.params; - workspace = await Workspace.findOne({ - _id: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace' - }); - } + workspace = await Workspace.findOne({ + _id: workspaceId, + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to get workspace", + }); + } - return res.status(200).send({ - workspace - }); + return res.status(200).send({ + workspace, + }); }; /** @@ -141,46 +140,46 @@ export const getWorkspace = async (req: Request, res: Response) => { * @returns */ export const createWorkspace = async (req: Request, res: Response) => { - let workspace; - try { - const { workspaceName, organizationId } = req.body; + let workspace; + try { + const { workspaceName, organizationId } = req.body; - // validate organization membership - const membershipOrg = await MembershipOrg.findOne({ - user: req.user._id, - organization: organizationId - }); + // validate organization membership + const membershipOrg = await MembershipOrg.findOne({ + user: req.user._id, + organization: organizationId, + }); - if (!membershipOrg) { - throw new Error('Failed to validate organization membership'); - } + if (!membershipOrg) { + throw new Error("Failed to validate organization membership"); + } - if (workspaceName.length < 1) { - throw new Error('Workspace names must be at least 1-character long'); - } + if (workspaceName.length < 1) { + throw new Error("Workspace names must be at least 1-character long"); + } - // create workspace and add user as member - workspace = await create({ - name: workspaceName, - organizationId - }); + // create workspace and add user as member + workspace = await create({ + name: workspaceName, + organizationId, + }); - await addMemberships({ - userIds: [req.user._id], - workspaceId: workspace._id.toString(), - roles: [ADMIN] - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to create workspace' - }); - } + await addMemberships({ + userIds: [req.user._id], + workspaceId: workspace._id.toString(), + roles: [ADMIN], + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to create workspace", + }); + } - return res.status(200).send({ - workspace - }); + return res.status(200).send({ + workspace, + }); }; /** @@ -190,24 +189,24 @@ export const createWorkspace = async (req: Request, res: Response) => { * @returns */ export const deleteWorkspace = async (req: Request, res: Response) => { - try { - const { workspaceId } = req.params; + try { + const { workspaceId } = req.params; - // delete workspace - await deleteWork({ - id: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete workspace' - }); - } + // delete workspace + await deleteWork({ + id: workspaceId, + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to delete workspace", + }); + } - return res.status(200).send({ - message: 'Successfully deleted workspace' - }); + return res.status(200).send({ + message: "Successfully deleted workspace", + }); }; /** @@ -217,34 +216,34 @@ export const deleteWorkspace = async (req: Request, res: Response) => { * @returns */ export const changeWorkspaceName = async (req: Request, res: Response) => { - let workspace; - try { - const { workspaceId } = req.params; - const { name } = req.body; + let workspace; + try { + const { workspaceId } = req.params; + const { name } = req.body; - workspace = await Workspace.findOneAndUpdate( - { - _id: workspaceId - }, - { - name - }, - { - new: true - } - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to change workspace name' - }); - } + workspace = await Workspace.findOneAndUpdate( + { + _id: workspaceId, + }, + { + name, + }, + { + new: true, + } + ); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to change workspace name", + }); + } - return res.status(200).send({ - message: 'Successfully changed workspace name', - workspace - }); + return res.status(200).send({ + message: "Successfully changed workspace name", + workspace, + }); }; /** @@ -254,24 +253,24 @@ export const changeWorkspaceName = async (req: Request, res: Response) => { * @returns */ export const getWorkspaceIntegrations = async (req: Request, res: Response) => { - let integrations; - try { - const { workspaceId } = req.params; + let integrations; + try { + const { workspaceId } = req.params; - integrations = await Integration.find({ - workspace: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace integrations' - }); - } + integrations = await Integration.find({ + workspace: workspaceId, + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to get workspace integrations", + }); + } - return res.status(200).send({ - integrations - }); + return res.status(200).send({ + integrations, + }); }; /** @@ -281,56 +280,56 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => { * @returns */ export const getWorkspaceIntegrationAuthorizations = async ( - req: Request, - res: Response + req: Request, + res: Response ) => { - let authorizations; - try { - const { workspaceId } = req.params; + let authorizations; + try { + const { workspaceId } = req.params; - authorizations = await IntegrationAuth.find({ - workspace: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace integration authorizations' - }); - } + authorizations = await IntegrationAuth.find({ + workspace: workspaceId, + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to get workspace integration authorizations", + }); + } - return res.status(200).send({ - authorizations - }); + return res.status(200).send({ + authorizations, + }); }; /** * Return service service tokens for workspace [workspaceId] belonging to user - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getWorkspaceServiceTokens = async ( - req: Request, - res: Response + req: Request, + res: Response ) => { - let serviceTokens; - try { - const { workspaceId } = req.params; - // ?? FIX. - serviceTokens = await ServiceToken.find({ - user: req.user._id, - workspace: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace service tokens' - }); - } - - return res.status(200).send({ - serviceTokens - }); -} \ No newline at end of file + let serviceTokens; + try { + const { workspaceId } = req.params; + // ?? FIX. + serviceTokens = await ServiceToken.find({ + user: req.user._id, + workspace: workspaceId, + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to get workspace service tokens", + }); + } + + return res.status(200).send({ + serviceTokens, + }); +}; diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index 7a0d5e1c5..b82dca9fe 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -246,13 +246,14 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async ( relatedWorkspace.environments.forEach(environment => { const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ }) const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE }) - if (isReadBlocked) { + if (isReadBlocked && isWriteBlocked) { return } else { accessibleEnvironments.push({ name: environment.name, slug: environment.slug, - isWriteDenied: isWriteBlocked + isWriteDenied: isWriteBlocked, + isReadDenied: isReadBlocked }) } }) diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 7267ae50c..29ef67000 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -17,7 +17,7 @@ import { EESecretService, EELogService } from '../../ee/services'; import { postHogClient } from '../../services'; import { getChannelFromUserAgent } from '../../utils/posthog'; import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; -import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; +import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions'; /** * Create secret(s) for workspace with id [workspaceId] and environment [environment] @@ -298,27 +298,42 @@ export const getSecrets = async (req: Request, res: Response) => { userEmail = req.serviceTokenData.user.email; } - // none service token case as service tokens are already scoped + // none service token case as service tokens are already scoped to env and project + let hasWriteOnlyAccess if (!req.serviceTokenData) { - const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ) - if (!hasAccess) { + hasWriteOnlyAccess = await userHasWriteOnlyAbility(userId, workspaceId, environment) + const hasNoAccess = await userHasNoAbility(userId, workspaceId, environment) + if (hasNoAccess) { throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) } } - - const [err, secrets] = await to(Secret.find( - { - workspace: workspaceId, - environment, - $or: [ - { user: userId }, - { user: { $exists: false } } - ], - type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } - } - ).populate("tags").then()) - - if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack }); + let secrets: any + if (hasWriteOnlyAccess) { + secrets = await Secret.find( + { + workspace: workspaceId, + environment, + $or: [ + { user: userId }, + { user: { $exists: false } } + ], + type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + } + ) + .select("secretKeyCiphertext secretKeyIV secretKeyTag") + } else { + secrets = await Secret.find( + { + workspace: workspaceId, + environment, + $or: [ + { user: userId }, + { user: { $exists: false } } + ], + type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + } + ).populate("tags") + } const channel = getChannelFromUserAgent(req.headers['user-agent']) @@ -356,6 +371,59 @@ export const getSecrets = async (req: Request, res: Response) => { }); } + +export const getOnlySecretKeys = async (req: Request, res: Response) => { + const { workspaceId, environment } = req.query; + + let userId = "" // used for getting personal secrets for user + let userEmail = "" // used for posthog + if (req.user) { + userId = req.user._id; + userEmail = req.user.email; + } + + if (req.serviceTokenData) { + userId = req.serviceTokenData.user._id + userEmail = req.serviceTokenData.user.email; + } + + // none service token case as service tokens are already scoped + if (!req.serviceTokenData) { + const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ) + if (!hasAccess) { + throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) + } + } + + const [err, secretKeys] = await to(Secret.find( + { + workspace: workspaceId, + environment, + $or: [ + { user: userId }, + { user: { $exists: false } } + ], + type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + } + ) + .select("secretKeyIV secretKeyTag secretKeyCiphertext") + .then()) + + if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack }); + + // readAction && await EELogService.createLog({ + // userId: new Types.ObjectId(userId), + // workspaceId: new Types.ObjectId(workspaceId as string), + // actions: [readAction], + // channel, + // ipAddress: req.ip + // }); + + return res.status(200).send({ + secretKeys + }); +} + /** * Update secret(s) * @param req diff --git a/backend/src/ee/helpers/checkMembershipPermissions.ts b/backend/src/ee/helpers/checkMembershipPermissions.ts index 55155e885..50cd28917 100644 --- a/backend/src/ee/helpers/checkMembershipPermissions.ts +++ b/backend/src/ee/helpers/checkMembershipPermissions.ts @@ -1,5 +1,6 @@ import _ from "lodash"; import { Membership } from "../../models"; +import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization"; export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => { const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) @@ -15,4 +16,39 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi } return true +} + +export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, environment: any) => { + const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) + if (!membershipForWorkspace) { + return false + } + + const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; + const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); + const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); + + // case: you have write only if read is blocked and write is not + if (isReadDisallowed && !isWriteDisallowed) { + return true + } + + return false +} + +export const userHasNoAbility = async (userId: any, workspaceId: any, environment: any) => { + const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) + if (!membershipForWorkspace) { + return true + } + + const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; + const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); + const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); + + if (isReadBlocked && isWriteDisallowed) { + return true + } + + return false } \ No newline at end of file diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index f0f30f897..7a828f332 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -1,7 +1,7 @@ -import axios from 'axios'; -import * as Sentry from '@sentry/node'; -import { Octokit } from '@octokit/rest'; -import { IIntegrationAuth } from '../models'; +import axios from "axios"; +import * as Sentry from "@sentry/node"; +import { Octokit } from "@octokit/rest"; +import { IIntegrationAuth } from "../models"; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, @@ -12,12 +12,14 @@ import { INTEGRATION_GITHUB, INTEGRATION_RENDER, INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, INTEGRATION_HEROKU_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, - INTEGRATION_FLYIO_API_URL -} from '../variables'; + INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, +} from "../variables"; /** * Return list of names of apps for integration named [integration] @@ -29,7 +31,7 @@ import { */ const getApps = async ({ integrationAuth, - accessToken + accessToken, }: { integrationAuth: IIntegrationAuth; accessToken: string; @@ -54,40 +56,45 @@ 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_RENDER: apps = await getAppsRender({ - accessToken + accessToken, }); break; case INTEGRATION_FLYIO: apps = await getAppsFlyio({ - accessToken + accessToken, + }); + break; + case INTEGRATION_CIRCLECI: + apps = await getAppsCircleCI({ + accessToken, }); break; } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get integration apps'); + throw new Error("Failed to get integration apps"); } return apps; @@ -106,19 +113,19 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { const res = ( await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { headers: { - Accept: 'application/vnd.heroku+json; version=3', - Authorization: `Bearer ${accessToken}` - } + Accept: "application/vnd.heroku+json; version=3", + Authorization: `Bearer ${accessToken}`, + }, }) ).data; apps = res.map((a: any) => ({ - name: a.name + name: a.name, })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Heroku integration apps'); + throw new Error("Failed to get Heroku integration apps"); } return apps; @@ -131,10 +138,10 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { * @returns {Object[]} apps - names of Vercel apps * @returns {String} apps.name - name of Vercel app */ -const getAppsVercel = async ({ +const getAppsVercel = async ({ integrationAuth, - accessToken -}: { + accessToken, +}: { integrationAuth: IIntegrationAuth; accessToken: string; }) => { @@ -146,21 +153,23 @@ const getAppsVercel = async ({ Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'application/json' }, - ...( integrationAuth?.teamId ? { - params: { - teamId: integrationAuth.teamId - } - } : {}) + ...(integrationAuth?.teamId + ? { + params: { + teamId: integrationAuth.teamId, + }, + } + : {}), }) ).data; apps = res.projects.map((a: any) => ({ - name: a.name + name: a.name, })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Vercel integration apps'); + throw new Error("Failed to get Vercel integration apps"); } return apps; @@ -173,11 +182,7 @@ const getAppsVercel = async ({ * @returns {Object[]} apps - names of Netlify sites * @returns {String} apps.name - name of Netlify site */ -const getAppsNetlify = async ({ - accessToken -}: { - accessToken: string; -}) => { +const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => { let apps; try { const res = ( @@ -191,12 +196,12 @@ const getAppsNetlify = async ({ apps = res.map((a: any) => ({ name: a.name, - appId: a.site_id + appId: a.site_id, })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Netlify integration apps'); + throw new Error("Failed to get Netlify integration apps"); } return apps; @@ -209,35 +214,32 @@ const getAppsNetlify = async ({ * @returns {Object[]} apps - names of Netlify sites * @returns {String} apps.name - name of Netlify site */ -const getAppsGithub = async ({ - accessToken -}: { - accessToken: string; -}) => { +const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { let apps; try { const octokit = new Octokit({ - auth: accessToken + auth: accessToken, }); - const repos = (await octokit.request( - 'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}', - { - per_page: 100 - } - )).data; + const repos = ( + await octokit.request( + "GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}", + { + per_page: 100, + } + ) + ).data; apps = repos - .filter((a:any) => a.permissions.admin === true) + .filter((a: any) => a.permissions.admin === true) .map((a: any) => ({ - name: a.name, - owner: a.owner.login - }) - ); + name: a.name, + owner: a.owner.login, + })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Github repos'); + throw new Error("Failed to get Github repos"); } return apps; @@ -251,11 +253,7 @@ const getAppsGithub = async ({ * @returns {String} apps.name - name of Render service * @returns {String} apps.appId - id of Render service */ -const getAppsRender = async ({ - accessToken -}: { - accessToken: string; -}) => { +const getAppsRender = async ({ accessToken }: { accessToken: string }) => { let apps: any; try { const res = ( @@ -263,8 +261,8 @@ const getAppsRender = async ({ headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json', - 'Accept-Encoding': 'application/json' - } + 'Accept-Encoding': 'application/json', + }, }) ).data; @@ -277,11 +275,11 @@ const getAppsRender = async ({ } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Render services'); + throw new Error("Failed to get Render services"); } - + return apps; -} +}; /** * Return list of apps for Fly.io integration @@ -290,11 +288,7 @@ const getAppsRender = async ({ * @returns {Object[]} apps - names and ids of Fly.io apps * @returns {String} apps.name - name of Fly.io apps */ -const getAppsFlyio = async ({ - accessToken -}: { - accessToken: string; -}) => { +const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { let apps; try { const query = ` @@ -308,34 +302,71 @@ const getAppsFlyio = async ({ } } `; - - const res = (await axios({ - url: INTEGRATION_FLYIO_API_URL, - method: 'post', - headers: { - 'Authorization': 'Bearer ' + accessToken, + + const res = ( + await axios({ + url: INTEGRATION_FLYIO_API_URL, + method: "post", + headers: { + Authorization: "Bearer " + accessToken, 'Accept': 'application/json', - 'Accept-Encoding': 'application/json' - }, - data: { - query, - variables: { - role: null - } - } - })).data.data.apps.nodes; - - apps = res - .map((a: any) => ({ - name: a.name - })); + 'Accept-Encoding': 'application/json', + }, + data: { + query, + variables: { + role: null, + }, + }, + }) + ).data.data.apps.nodes; + + apps = res.map((a: any) => ({ + name: a.name, + })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Fly.io apps'); + throw new Error("Failed to get Fly.io apps"); + } + + return apps; +}; + +/** + * Return list of projects for CircleCI integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for CircleCI API + * @returns {Object[]} apps - + * @returns {String} apps.name - name of CircleCI apps + */ +const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => { + let apps: any; + try { + const res = ( + await axios.get( + `${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`, + { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json", + }, + } + ) + ).data + + apps = res?.map((a: any) => { + return { + name: a?.reponame + } + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to get CircleCI projects"); } return apps; -} +}; export { getApps }; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 222fa940a..e4bdc116c 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1,5 +1,5 @@ -import axios from 'axios'; -import * as Sentry from '@sentry/node'; +import axios from "axios"; +import * as Sentry from "@sentry/node"; import _ from 'lodash'; import AWS from 'aws-sdk'; import { @@ -9,9 +9,9 @@ import { GetSecretValueCommand, ResourceNotFoundException } from '@aws-sdk/client-secrets-manager'; -import { Octokit } from '@octokit/rest'; -import sodium from 'libsodium-wrappers'; -import { IIntegration, IIntegrationAuth } from '../models'; +import { Octokit } from "@octokit/rest"; +import sodium from "libsodium-wrappers"; +import { IIntegration, IIntegrationAuth } from "../models"; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, @@ -22,12 +22,15 @@ import { INTEGRATION_GITHUB, INTEGRATION_RENDER, INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, INTEGRATION_HEROKU_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, - INTEGRATION_FLYIO_API_URL -} from '../variables'; + INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, +} from "../variables"; +import { access, appendFile } from "fs"; /** * Sync/push [secrets] to [app] in integration named [integration] @@ -43,7 +46,7 @@ const syncSecrets = async ({ integrationAuth, secrets, accessId, - accessToken + accessToken, }: { integration: IIntegration; integrationAuth: IIntegrationAuth; @@ -80,7 +83,7 @@ const syncSecrets = async ({ await syncSecretsHeroku({ integration, secrets, - accessToken + accessToken, }); break; case INTEGRATION_VERCEL: @@ -88,7 +91,7 @@ const syncSecrets = async ({ integration, integrationAuth, secrets, - accessToken + accessToken, }); break; case INTEGRATION_NETLIFY: @@ -96,30 +99,36 @@ const syncSecrets = async ({ integration, integrationAuth, secrets, - accessToken + accessToken, }); break; case INTEGRATION_GITHUB: await syncSecretsGitHub({ integration, secrets, - accessToken + accessToken, }); break; case INTEGRATION_RENDER: await syncSecretsRender({ integration, secrets, - accessToken + accessToken, }); break; case INTEGRATION_FLYIO: await syncSecretsFlyio({ integration, secrets, - accessToken + accessToken, }); break; + case INTEGRATION_CIRCLECI: + await syncSecretsCircleCI({ + integration, + secrets, + accessToken, + }); } } catch (err) { Sentry.setUser(null); @@ -465,7 +474,7 @@ const syncSecretsAWSSecretManager = async ({ const syncSecretsHeroku = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; @@ -477,9 +486,9 @@ const syncSecretsHeroku = async ({ `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, { headers: { - Accept: 'application/vnd.heroku+json; version=3', - Authorization: `Bearer ${accessToken}` - } + Accept: "application/vnd.heroku+json; version=3", + Authorization: `Bearer ${accessToken}`, + }, } ) ).data; @@ -495,15 +504,15 @@ const syncSecretsHeroku = async ({ secrets, { headers: { - Accept: 'application/vnd.heroku+json; version=3', - Authorization: `Bearer ${accessToken}` - } + Accept: "application/vnd.heroku+json; version=3", + Authorization: `Bearer ${accessToken}`, + }, } ); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to sync secrets to Heroku'); + throw new Error("Failed to sync secrets to Heroku"); } }; @@ -514,37 +523,42 @@ const syncSecretsHeroku = async ({ * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) */ const syncSecretsVercel = async ({ - integration, - integrationAuth, - secrets, - accessToken + integration, + integrationAuth, + secrets, + accessToken, }: { - integration: IIntegration, - integrationAuth: IIntegrationAuth, - secrets: any; - accessToken: string; + integration: IIntegration; + integrationAuth: IIntegrationAuth; + secrets: any; + accessToken: string; }) => { - interface VercelSecret { - id?: string; - type: string; - key: string; - value: string; - target: string[]; - } - - try { - // Get all (decrypted) secrets back from Vercel in - // decrypted format - const params: { [key: string]: string } = { - decrypt: 'true', - ...( integrationAuth?.teamId ? { - teamId: integrationAuth.teamId - } : {}) - } - - const res = (await Promise.all((await axios.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, - { + interface VercelSecret { + id?: string; + type: string; + key: string; + value: string; + target: string[]; + } + + try { + // Get all (decrypted) secrets back from Vercel in + // decrypted format + const params: { [key: string]: string } = { + decrypt: "true", + ...(integrationAuth?.teamId + ? { + teamId: integrationAuth.teamId, + } + : {}), + }; + + const res = ( + await Promise.all( + ( + await axios.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, + { params, headers: { Authorization: `Bearer ${accessToken}` @@ -572,98 +586,95 @@ const syncSecretsVercel = async ({ const deleteSecrets: VercelSecret[] = []; const newSecrets: VercelSecret[] = []; - // Identify secrets to create - Object.keys(secrets).map((key) => { - if (!(key in res)) { - // case: secret has been created - newSecrets.push({ - key: key, - value: secrets[key], - type: 'encrypted', - target: [integration.targetEnvironment] - }); - } - }); - - // Identify secrets to update and delete - Object.keys(res).map((key) => { - if (key in secrets) { - if (res[key].value !== secrets[key]) { - // case: secret value has changed - updateSecrets.push({ - id: res[key].id, - key: key, - value: secrets[key], - type: 'encrypted', - target: [integration.targetEnvironment] - }); - } - } else { - // case: secret has been deleted - deleteSecrets.push({ - id: res[key].id, - key: key, - value: res[key].value, - type: 'encrypted', - target: [integration.targetEnvironment], - }); - } - }); - - // Sync/push new secrets - if (newSecrets.length > 0) { - await axios.post( - `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, - newSecrets, - { - params, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); + // Identify secrets to create + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: secret has been created + newSecrets.push({ + key: key, + value: secrets[key], + type: "encrypted", + target: [integration.targetEnvironment], + }); } + }); - // Sync/push updated secrets - if (updateSecrets.length > 0) { - updateSecrets.forEach(async (secret: VercelSecret) => { - const { - id, - ...updatedSecret - } = secret; - await axios.patch( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - updatedSecret, - { - params, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); + // Identify secrets to update and delete + Object.keys(res).map((key) => { + if (key in secrets) { + if (res[key].value !== secrets[key]) { + // case: secret value has changed + updateSecrets.push({ + id: res[key].id, + key: key, + value: secrets[key], + type: "encrypted", + target: [integration.targetEnvironment], }); + } + } else { + // case: secret has been deleted + deleteSecrets.push({ + id: res[key].id, + key: key, + value: res[key].value, + type: "encrypted", + target: [integration.targetEnvironment], + }); } + }); - // Delete secrets - if (deleteSecrets.length > 0) { - deleteSecrets.forEach(async (secret: VercelSecret) => { - await axios.delete( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to Vercel'); + // Sync/push new secrets + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, + newSecrets, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); } -} + + // Sync/push updated secrets + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: VercelSecret) => { + const { id, ...updatedSecret } = secret; + await axios.patch( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + updatedSecret, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + + // Delete secrets + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (secret: VercelSecret) => { + await axios.delete( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to sync secrets to Vercel"); + } +}; /** * Sync/push [secrets] to Netlify site with id [integration.appId] @@ -674,202 +685,214 @@ const syncSecretsVercel = async ({ * @param {Object} obj.accessToken - access token for Netlify integration */ const syncSecretsNetlify = async ({ - integration, - integrationAuth, - secrets, - accessToken + integration, + integrationAuth, + secrets, + accessToken, }: { - integration: IIntegration; - integrationAuth: IIntegrationAuth; - secrets: any; - accessToken: string; + integration: IIntegration; + integrationAuth: IIntegrationAuth; + secrets: any; + accessToken: string; }) => { - try { - - interface NetlifyValue { - id?: string; - context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', - value: string; - } - - interface NetlifySecret { - key: string; - values: NetlifyValue[]; - } - - interface NetlifySecretsRes { - [index: string]: NetlifySecret; - } - - const getParams = new URLSearchParams({ - context_name: 'all', // integration.context or all - site_id: integration.appId - }); - - const res = (await axios.get( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, - { - params: getParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - )) - .data - .reduce((obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret - }), {}); - - const newSecrets: NetlifySecret[] = []; // createEnvVars - const deleteSecrets: string[] = []; // deleteEnvVar - const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue - const updateSecrets: NetlifySecret[] = []; // setEnvVarValue - - // identify secrets to create and update - Object.keys(secrets).map((key) => { - if (!(key in res)) { - // case: Infisical secret does not exist in Netlify -> create secret - newSecrets.push({ - key, - values: [{ - value: secrets[key], - context: integration.targetEnvironment - }] - }); - } else { - // case: Infisical secret exists in Netlify - const contexts = res[key].values - .reduce((obj: any, value: NetlifyValue) => ({ - ...obj, - [value.context]: value - }), {}); - - if (integration.targetEnvironment in contexts) { - // case: Netlify secret value exists in integration context - if (secrets[key] !== contexts[integration.targetEnvironment].value) { - // case: Infisical and Netlify secret values are different - // -> update Netlify secret context and value - updateSecrets.push({ - key, - values: [{ - context: integration.targetEnvironment, - value: secrets[key] - }] - }); - } - } else { - // case: Netlify secret value does not exist in integration context - // -> add the new Netlify secret context and value - updateSecrets.push({ - key, - values: [{ - context: integration.targetEnvironment, - value: secrets[key] - }] - }); - } - } - }) - - // identify secrets to delete - // TODO: revise (patch case where 1 context was deleted but others still there - Object.keys(res).map((key) => { - // loop through each key's context - if (!(key in secrets)) { - // case: Netlify secret does not exist in Infisical - - const numberOfValues = res[key].values.length; - - res[key].values.forEach((value: NetlifyValue) => { - if (value.context === integration.targetEnvironment) { - if (numberOfValues <= 1) { - // case: Netlify secret value has less than 1 context -> delete secret - deleteSecrets.push(key); - } else { - // case: Netlify secret value has more than 1 context -> delete secret value context - deleteSecretValues.push({ - key, - values: [{ - id: value.id, - context: integration.targetEnvironment, - value: value.value - }] - }); - } - } - }); - } - }); - - const syncParams = new URLSearchParams({ - site_id: integration.appId - }); - - if (newSecrets.length > 0) { - await axios.post( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, - newSecrets, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - } - - if (updateSecrets.length > 0) { - updateSecrets.forEach(async (secret: NetlifySecret) => { - await axios.patch( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, - { - context: secret.values[0].context, - value: secret.values[0].value - }, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } - - if (deleteSecrets.length > 0) { - deleteSecrets.forEach(async (key: string) => { - await axios.delete( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } - - if (deleteSecretValues.length > 0) { - deleteSecretValues.forEach(async (secret: NetlifySecret) => { - await axios.delete( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to Heroku'); + try { + interface NetlifyValue { + id?: string; + context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', + value: string; } -} + + interface NetlifySecret { + key: string; + values: NetlifyValue[]; + } + + interface NetlifySecretsRes { + [index: string]: NetlifySecret; + } + + const getParams = new URLSearchParams({ + context_name: "all", // integration.context or all + site_id: integration.appId, + }); + + const res = ( + await axios.get( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + { + params: getParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + ).data.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret, + }), + {} + ); + + const newSecrets: NetlifySecret[] = []; // createEnvVars + const deleteSecrets: string[] = []; // deleteEnvVar + const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue + const updateSecrets: NetlifySecret[] = []; // setEnvVarValue + + // identify secrets to create and update + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: Infisical secret does not exist in Netlify -> create secret + newSecrets.push({ + key, + values: [ + { + value: secrets[key], + context: integration.targetEnvironment, + }, + ], + }); + } else { + // case: Infisical secret exists in Netlify + const contexts = res[key].values.reduce( + (obj: any, value: NetlifyValue) => ({ + ...obj, + [value.context]: value, + }), + {} + ); + + if (integration.targetEnvironment in contexts) { + // case: Netlify secret value exists in integration context + if (secrets[key] !== contexts[integration.targetEnvironment].value) { + // case: Infisical and Netlify secret values are different + // -> update Netlify secret context and value + updateSecrets.push({ + key, + values: [ + { + context: integration.targetEnvironment, + value: secrets[key], + }, + ], + }); + } + } else { + // case: Netlify secret value does not exist in integration context + // -> add the new Netlify secret context and value + updateSecrets.push({ + key, + values: [ + { + context: integration.targetEnvironment, + value: secrets[key], + }, + ], + }); + } + } + }); + + // identify secrets to delete + // TODO: revise (patch case where 1 context was deleted but others still there + Object.keys(res).map((key) => { + // loop through each key's context + if (!(key in secrets)) { + // case: Netlify secret does not exist in Infisical + + const numberOfValues = res[key].values.length; + + res[key].values.forEach((value: NetlifyValue) => { + if (value.context === integration.targetEnvironment) { + if (numberOfValues <= 1) { + // case: Netlify secret value has less than 1 context -> delete secret + deleteSecrets.push(key); + } else { + // case: Netlify secret value has more than 1 context -> delete secret value context + deleteSecretValues.push({ + key, + values: [ + { + id: value.id, + context: integration.targetEnvironment, + value: value.value, + }, + ], + }); + } + } + }); + } + }); + + const syncParams = new URLSearchParams({ + site_id: integration.appId, + }); + + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + newSecrets, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + } + + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: NetlifySecret) => { + await axios.patch( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, + { + context: secret.values[0].context, + value: secret.values[0].value, + }, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (key: string) => { + await axios.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + + if (deleteSecretValues.length > 0) { + deleteSecretValues.forEach(async (secret: NetlifySecret) => { + await axios.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to sync secrets to Heroku"); + } +}; /** * Sync/push [secrets] to GitHub repo with name [integration.app] @@ -877,24 +900,23 @@ const syncSecretsNetlify = async ({ * @param {IIntegration} obj.integration - integration details * @param {IIntegrationAuth} obj.integrationAuth - integration auth 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 GitHub integration + * @param {String} obj.accessToken - access token for GitHub integration */ const syncSecretsGitHub = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; accessToken: string; }) => { try { - interface GitHubRepoKey { key_id: string; key: string; } - + interface GitHubSecret { name: string; created_at: string; @@ -902,87 +924,88 @@ const syncSecretsGitHub = async ({ } interface GitHubSecretRes { - [index: string]: GitHubSecret; + [index: string]: GitHubSecret; } const deleteSecrets: GitHubSecret[] = []; const octokit = new Octokit({ - auth: accessToken + auth: accessToken, }); // const user = (await octokit.request('GET /user', {})).data; - const repoPublicKey: GitHubRepoKey = (await octokit.request( - 'GET /repos/{owner}/{repo}/actions/secrets/public-key', - { - owner: integration.owner, - repo: integration.app - } - )).data; + const repoPublicKey: GitHubRepoKey = ( + await octokit.request( + "GET /repos/{owner}/{repo}/actions/secrets/public-key", + { + owner: integration.owner, + repo: integration.app, + } + ) + ).data; // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key - const encryptedSecrets: GitHubSecretRes = (await octokit.request( - 'GET /repos/{owner}/{repo}/actions/secrets', - { + const encryptedSecrets: GitHubSecretRes = ( + await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", { owner: integration.owner, - repo: integration.app - } - )) - .data - .secrets - .reduce((obj: any, secret: any) => ({ - ...obj, - [secret.name]: secret - }), {}); - + repo: integration.app, + }) + ).data.secrets.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.name]: secret, + }), + {} + ); + Object.keys(encryptedSecrets).map(async (key) => { if (!(key in secrets)) { await octokit.request( - 'DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}', + "DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { owner: integration.owner, repo: integration.app, - secret_name: key + secret_name: key, } ); } }); - + Object.keys(secrets).map((key) => { // let encryptedSecret; sodium.ready.then(async () => { - // convert secret & base64 key to Uint8Array. - const binkey = sodium.from_base64( - repoPublicKey.key, - sodium.base64_variants.ORIGINAL - ); - const binsec = sodium.from_string(secrets[key]); + // convert secret & base64 key to Uint8Array. + const binkey = sodium.from_base64( + repoPublicKey.key, + sodium.base64_variants.ORIGINAL + ); + const binsec = sodium.from_string(secrets[key]); - // encrypt secret using libsodium - const encBytes = sodium.crypto_box_seal(binsec, binkey); + // encrypt secret using libsodium + const encBytes = sodium.crypto_box_seal(binsec, binkey); - // convert encrypted Uint8Array to base64 - const encryptedSecret = sodium.to_base64( - encBytes, - sodium.base64_variants.ORIGINAL - ); - - await octokit.request( - 'PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}', - { - owner: integration.owner, - repo: integration.app, - secret_name: key, - encrypted_value: encryptedSecret, - key_id: repoPublicKey.key_id - } - ); + // convert encrypted Uint8Array to base64 + const encryptedSecret = sodium.to_base64( + encBytes, + sodium.base64_variants.ORIGINAL + ); + + await octokit.request( + "PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", + { + owner: integration.owner, + repo: integration.app, + secret_name: key, + encrypted_value: encryptedSecret, + key_id: repoPublicKey.key_id, + } + ); }); }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to sync secrets to GitHub'); + throw new Error("Failed to sync secrets to GitHub"); } }; @@ -996,7 +1019,7 @@ const syncSecretsGitHub = async ({ const syncSecretsRender = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; @@ -1007,20 +1030,20 @@ const syncSecretsRender = async ({ `${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`, Object.keys(secrets).map((key) => ({ key, - value: secrets[key] + value: secrets[key], })), { headers: { - Authorization: `Bearer ${accessToken}` - } + Authorization: `Bearer ${accessToken}`, + }, } ); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to sync secrets to Render'); + throw new Error("Failed to sync secrets to Render"); } -} +}; /** * Sync/push [secrets] to Fly.io app @@ -1032,7 +1055,7 @@ const syncSecretsRender = async ({ const syncSecretsFlyio = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; @@ -1062,28 +1085,31 @@ const syncSecretsFlyio = async ({ await axios({ url: INTEGRATION_FLYIO_API_URL, - method: 'post', + method: "post", headers: { - 'Authorization': 'Bearer ' + accessToken + Authorization: "Bearer " + accessToken, }, data: { query: SetSecrets, variables: { input: { appId: integration.app, - secrets: Object.entries(secrets).map(([key, value]) => ({ key, value })) - } - } - } + secrets: Object.entries(secrets).map(([key, value]) => ({ + key, + value, + })), + }, + }, + }, }); - + // get secrets interface FlyioSecret { name: string; digest: string; createdAt: string; } - + const GetSecrets = `query ($appName: String!) { app(name: $appName) { secrets { @@ -1094,8 +1120,9 @@ const syncSecretsFlyio = async ({ } }`; - const getSecretsRes = (await axios({ - method: 'post', + const getSecretsRes = ( + await axios({ + method: "post", url: INTEGRATION_FLYIO_API_URL, headers: { 'Authorization': 'Bearer ' + accessToken, @@ -1105,15 +1132,16 @@ const syncSecretsFlyio = async ({ data: { query: GetSecrets, variables: { - appName: integration.app - } - } - })).data.data.app.secrets; - + appName: integration.app, + }, + }, + }) + ).data.data.app.secrets; + const deleteSecretsKeys = getSecretsRes .filter((secret: FlyioSecret) => !(secret.name in secrets)) .map((secret: FlyioSecret) => secret.name); - + // unset (delete) secrets const DeleteSecrets = `mutation($input: UnsetSecretsInput!) { unsetSecrets(input: $input) { @@ -1134,28 +1162,107 @@ const syncSecretsFlyio = async ({ }`; await axios({ - method: 'post', - url: INTEGRATION_FLYIO_API_URL, - headers: { - 'Authorization': 'Bearer ' + accessToken, - 'Content-Type': 'application/json' + method: "post", + url: INTEGRATION_FLYIO_API_URL, + headers: { + Authorization: "Bearer " + accessToken, + "Content-Type": "application/json", + }, + data: { + query: DeleteSecrets, + variables: { + input: { + appId: integration.app, + keys: deleteSecretsKeys, + }, }, - data: { - query: DeleteSecrets, - variables: { - input: { - appId: integration.app, - keys: deleteSecretsKeys - } - } - } + }, }); - } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to sync secrets to Fly.io'); + throw new Error("Failed to sync secrets to Fly.io"); } -} +}; -export { syncSecrets }; \ No newline at end of file +/** + * Sync/push [secrets] to CircleCI project + * @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 CircleCI integration + */ +const syncSecretsCircleCI = async ({ + integration, + secrets, + accessToken, +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + try { + const circleciOrganizationDetail = ( + await axios.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json", + }, + }) + ).data[0]; + + const { slug } = circleciOrganizationDetail; + + // sync secrets to CircleCI + Object.keys(secrets).forEach( + async (key) => + await axios.post( + `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, + { + name: key, + value: secrets[key], + }, + { + headers: { + "Circle-Token": accessToken, + "Content-Type": "application/json", + }, + } + ) + ); + + // get secrets from CircleCI + const getSecretsRes = ( + await axios.get( + `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, + { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json", + }, + } + ) + ).data?.items; + + // delete secrets from CircleCI + getSecretsRes.forEach(async (sec: any) => { + if (!(sec.name in secrets)) { + await axios.delete( + `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`, + { + headers: { + "Circle-Token": accessToken, + "Content-Type": "application/json", + }, + } + ); + } + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to sync secrets to CircleCI"); + } +}; + +export { syncSecrets }; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index ddf31feb7..1b52fabd9 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, model, Types } from "mongoose"; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, @@ -8,8 +8,9 @@ import { INTEGRATION_NETLIFY, INTEGRATION_GITHUB, INTEGRATION_RENDER, - INTEGRATION_FLYIO -} from '../variables'; + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, +} from "../variables"; export interface IIntegration { _id: Types.ObjectId; @@ -31,44 +32,47 @@ export interface IIntegration { | 'netlify' | 'github' | 'render' - | 'flyio'; + | 'flyio' + | 'circleci'; integrationAuth: Types.ObjectId; } const integrationSchema = new Schema( { workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, }, environment: { type: String, - required: true + required: true, }, isActive: { type: Boolean, - required: true + required: true, }, app: { // name of app in provider type: String, - default: null + default: null, }, - appId: { // (new) + appId: { + // (new) // id of app in provider type: String, - default: null + default: null, }, - targetEnvironment: { // (new) - // target environment + targetEnvironment: { + // (new) + // target environment type: String, - default: null + default: null, }, owner: { // github-specific repo owner-login type: String, - default: null + default: null, }, path: { // aws-parameter-store-specific path @@ -91,21 +95,22 @@ const integrationSchema = new Schema( INTEGRATION_NETLIFY, INTEGRATION_GITHUB, INTEGRATION_RENDER, - INTEGRATION_FLYIO + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, ], - required: true + required: true, }, integrationAuth: { type: Schema.Types.ObjectId, - ref: 'IntegrationAuth', - required: true - } + ref: "IntegrationAuth", + required: true, + }, }, { - timestamps: true + timestamps: true, } ); -const Integration = model('Integration', integrationSchema); +const Integration = model("Integration", integrationSchema); export default Integration; diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index ebe9daa55..95f8c75af 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, model, Types } from "mongoose"; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, @@ -8,24 +8,16 @@ import { INTEGRATION_NETLIFY, INTEGRATION_GITHUB, INTEGRATION_RENDER, - INTEGRATION_FLYIO -} from '../variables'; + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, +} from "../variables"; export interface IIntegrationAuth { _id: Types.ObjectId; workspace: Types.ObjectId; - integration: - | 'azure-key-vault' - | 'aws-parameter-store' - | 'aws-secret-manager' - | 'heroku' - | 'vercel' - | 'netlify' - | 'github' - | 'render' - | 'flyio'; - teamId: string; // TODO: deprecate (vercel) -> move to accessId - accountId: string; // TODO: deprecate (netlify) -> move to accessId + integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'aws-parameter-store' | 'aws-secret-manager'; + teamId: string; + accountId: string; refreshCiphertext?: string; refreshIV?: string; refreshTag?: string; @@ -41,9 +33,9 @@ export interface IIntegrationAuth { const integrationAuthSchema = new Schema( { workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, }, integration: { type: String, @@ -56,29 +48,30 @@ const integrationAuthSchema = new Schema( INTEGRATION_NETLIFY, INTEGRATION_GITHUB, INTEGRATION_RENDER, - INTEGRATION_FLYIO + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, ], - required: true + required: true, }, teamId: { // vercel-specific integration param - type: String + type: String, }, accountId: { // netlify-specific integration param - type: String + type: String, }, refreshCiphertext: { type: String, - select: false + select: false, }, refreshIV: { type: String, - select: false + select: false, }, refreshTag: { type: String, - select: false + select: false, }, accessIdCiphertext: { type: String, @@ -94,28 +87,28 @@ const integrationAuthSchema = new Schema( }, accessCiphertext: { type: String, - select: false + select: false, }, accessIV: { type: String, - select: false + select: false, }, accessTag: { type: String, - select: false + select: false, }, accessExpiresAt: { type: Date, - select: false - } + select: false, + }, }, { - timestamps: true + timestamps: true, } ); const IntegrationAuth = model( - 'IntegrationAuth', + "IntegrationAuth", integrationAuthSchema ); diff --git a/backend/src/models/secretApprovalRequest.ts b/backend/src/models/secretApprovalRequest.ts new file mode 100644 index 000000000..9fa897e25 --- /dev/null +++ b/backend/src/models/secretApprovalRequest.ts @@ -0,0 +1,83 @@ +import mongoose, { Schema, model } from 'mongoose'; +import Secret, { ISecret } from './secret'; + +interface ISecretApprovalRequest { + secret: mongoose.Types.ObjectId; + requestedChanges: ISecret; + requestedBy: mongoose.Types.ObjectId; + approvers: IApprover[]; + status: ApprovalStatus; + timestamp: Date; + requestType: RequestType; + requestId: string; +} + +interface IApprover { + userId: mongoose.Types.ObjectId; + status: ApprovalStatus; +} + +export enum ApprovalStatus { + PENDING = 'pending', + APPROVED = 'approved', + REJECTED = 'rejected' +} + +export enum RequestType { + UPDATE = 'update', + DELETE = 'delete', + CREATE = 'create' +} + +const approverSchema = new mongoose.Schema({ + user: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + status: { + type: String, + enum: [ApprovalStatus], + default: ApprovalStatus.PENDING + } +}); + +const secretApprovalRequestSchema = new Schema( + { + secret: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Secret' + }, + requestedChanges: Secret, + requestedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + approvers: [approverSchema], + status: { + type: String, + enum: ApprovalStatus, + default: ApprovalStatus.PENDING + }, + timestamp: { + type: Date, + default: Date.now + }, + requestType: { + type: String, + enum: RequestType, + required: true + }, + requestId: { + type: String, + required: false + } + }, + { + timestamps: true + } +); + +const SecretApprovalRequest = model('SecretApprovalRequest', secretApprovalRequestSchema); + +export default SecretApprovalRequest; diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index 182b23db1..52feec126 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -3,8 +3,8 @@ import { ENV_TESTING, ENV_STAGING, ENV_PROD, - ENV_SET -} from './environment'; + ENV_SET, +} from "./environment"; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, @@ -15,6 +15,7 @@ import { INTEGRATION_GITHUB, INTEGRATION_RENDER, INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, INTEGRATION_SET, INTEGRATION_OAUTH2, INTEGRATION_AZURE_TOKEN_URL, @@ -27,27 +28,22 @@ import { INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, - INTEGRATION_OPTIONS -} from './integration'; -import { - OWNER, - ADMIN, - MEMBER, - INVITED, - ACCEPTED, -} from './organization'; -import { SECRET_SHARED, SECRET_PERSONAL } from './secret'; -import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event'; + INTEGRATION_CIRCLECI_API_URL, + INTEGRATION_OPTIONS, +} from "./integration"; +import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization"; +import { SECRET_SHARED, SECRET_PERSONAL } from "./secret"; +import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from "./event"; import { ACTION_LOGIN, ACTION_LOGOUT, ACTION_ADD_SECRETS, ACTION_UPDATE_SECRETS, ACTION_DELETE_SECRETS, - ACTION_READ_SECRETS -} from './action'; -import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp'; -import { PLAN_STARTER, PLAN_PRO } from './stripe'; + ACTION_READ_SECRETS, +} from "./action"; +import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from "./smtp"; +import { PLAN_STARTER, PLAN_PRO } from "./stripe"; export { OWNER, @@ -71,6 +67,7 @@ export { INTEGRATION_GITHUB, INTEGRATION_RENDER, INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, INTEGRATION_SET, INTEGRATION_OAUTH2, INTEGRATION_AZURE_TOKEN_URL, @@ -83,6 +80,7 @@ export { INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS, ACTION_LOGIN, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index d22059cb9..3ef8d6f1e 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -3,50 +3,53 @@ import { TENANT_ID_AZURE } from '../config'; import { - CLIENT_ID_HEROKU, - CLIENT_ID_NETLIFY, - CLIENT_ID_GITHUB, - CLIENT_SLUG_VERCEL -} from '../config'; + CLIENT_ID_HEROKU, + CLIENT_ID_NETLIFY, + CLIENT_ID_GITHUB, + CLIENT_SLUG_VERCEL, +} from "../config"; // integrations const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault'; const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store'; const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager'; -const INTEGRATION_HEROKU = 'heroku'; -const INTEGRATION_VERCEL = 'vercel'; -const INTEGRATION_NETLIFY = 'netlify'; -const INTEGRATION_GITHUB = 'github'; -const INTEGRATION_RENDER = 'render'; -const INTEGRATION_FLYIO = 'flyio'; +const INTEGRATION_HEROKU = "heroku"; +const INTEGRATION_VERCEL = "vercel"; +const INTEGRATION_NETLIFY = "netlify"; +const INTEGRATION_GITHUB = "github"; +const INTEGRATION_RENDER = "render"; +const INTEGRATION_FLYIO = "flyio"; +const INTEGRATION_CIRCLECI = "circleci"; const INTEGRATION_SET = new Set([ INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_RENDER, - INTEGRATION_FLYIO + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_RENDER, + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, ]); // integration types -const INTEGRATION_OAUTH2 = 'oauth2'; +const INTEGRATION_OAUTH2 = "oauth2"; // integration oauth endpoints const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`; const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token'; const INTEGRATION_VERCEL_TOKEN_URL = - 'https://api.vercel.com/v2/oauth/access_token'; -const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token'; + "https://api.vercel.com/v2/oauth/access_token"; +const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token"; const INTEGRATION_GITHUB_TOKEN_URL = - 'https://github.com/login/oauth/access_token'; + "https://github.com/login/oauth/access_token"; // integration apps endpoints -const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com'; -const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com'; -const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com'; -const INTEGRATION_RENDER_API_URL = 'https://api.render.com'; -const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql'; +const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com"; +const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; +const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com"; +const INTEGRATION_RENDER_API_URL = "https://api.render.com"; +const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql"; +const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; const INTEGRATION_OPTIONS = [ { @@ -122,6 +125,15 @@ const INTEGRATION_OPTIONS = [ clientId: '', docsLink: '' }, + { + name: 'Circle CI', + slug: 'circleci', + image: 'Circle CI.png', + isAvailable: true, + type: 'pat', + clientId: '', + docsLink: '' + }, { name: 'Azure Key Vault', slug: 'azure-key-vault', @@ -149,15 +161,6 @@ const INTEGRATION_OPTIONS = [ type: '', clientId: '', docsLink: '' - }, - { - name: 'Circle CI', - slug: 'circleci', - image: 'Circle CI.png', - isAvailable: false, - type: '', - clientId: '', - docsLink: '' } ] @@ -165,23 +168,25 @@ export { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_RENDER, - INTEGRATION_FLYIO, - INTEGRATION_SET, - INTEGRATION_OAUTH2, + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_RENDER, + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, + INTEGRATION_SET, + INTEGRATION_OAUTH2, INTEGRATION_AZURE_TOKEN_URL, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_GITHUB_TOKEN_URL, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_RENDER_API_URL, - INTEGRATION_FLYIO_API_URL, - INTEGRATION_OPTIONS + INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_RENDER_API_URL, + INTEGRATION_FLYIO_API_URL, + INTEGRATION_CIRCLECI_API_URL, + INTEGRATION_OPTIONS, }; diff --git a/backend/src/variables/organization.ts b/backend/src/variables/organization.ts index 806460997..91af2ff25 100644 --- a/backend/src/variables/organization.ts +++ b/backend/src/variables/organization.ts @@ -1,24 +1,16 @@ // membership roles -const OWNER = 'owner'; -const ADMIN = 'admin'; -const MEMBER = 'member'; +const OWNER = "owner"; +const ADMIN = "admin"; +const MEMBER = "member"; // membership statuses -const INVITED = 'invited'; +const INVITED = "invited"; // membership permissions ability -const ABILITY_READ = 'read'; -const ABILITY_WRITE = 'write'; +const ABILITY_READ = "read"; +const ABILITY_WRITE = "write"; // -- organization -const ACCEPTED = 'accepted'; +const ACCEPTED = "accepted"; -export { - OWNER, - ADMIN, - MEMBER, - INVITED, - ACCEPTED, - ABILITY_READ, - ABILITY_WRITE -} \ No newline at end of file +export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED, ABILITY_READ, ABILITY_WRITE }; diff --git a/cli/docker/Dockerfile b/cli/docker/Dockerfile new file mode 100644 index 000000000..0436d4d8e --- /dev/null +++ b/cli/docker/Dockerfile @@ -0,0 +1,4 @@ +FROM alpine +RUN apk add --no-cache tini +COPY infisical /bin/infisical +ENTRYPOINT ["/sbin/tini", "--", "/bin/infisical"] \ No newline at end of file diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 39d6542c9..804df78c2 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -37,5 +37,6 @@ Configuring Infisical requires setting some environment variables. There is a fi | `CLIENT_SECRET_VERCEL` | OAuth2 client secret for Vercel integration | `None` | | `CLIENT_SECRET_NETLIFY` | OAuth2 client secret for Netlify integration | `None` | | `CLIENT_SECRET_GITHUB` | OAuth2 client secret for GitHub integration | `None` | -| `CLIENT_SLUG_VERCEL` | OAuth2 slug for Netlify integration | `None` | +| `CLIENT_SLUG_VERCEL` | OAuth2 slug for Netlify integration | `None` | | `SENTRY_DSN` | DSN for error-monitoring with Sentry | `None` | +| `INVITE_ONLY_SIGNUP` | If true, users can only sign up if they are invited | `false` | diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 7519acc28..f2e3acf70 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -11,7 +11,8 @@ const integrationSlugNameMapping: Mapping = { 'netlify': 'Netlify', 'github': 'GitHub', 'render': 'Render', - 'flyio': 'Fly.io' + 'flyio': 'Fly.io', + "circleci": 'CircleCI' } const envMapping: Mapping = { diff --git a/frontend/public/data/frequentInterfaces.ts b/frontend/public/data/frequentInterfaces.ts index 9865d9909..fa6c73a57 100644 --- a/frontend/public/data/frequentInterfaces.ts +++ b/frontend/public/data/frequentInterfaces.ts @@ -10,7 +10,7 @@ export interface Tag { export interface SecretDataProps { pos: number; key: string; - value: string; + value: string | undefined; valueOverride: string | undefined; id: string; comment: string; diff --git a/frontend/src/components/basic/table/ProjectUsersTable.tsx b/frontend/src/components/basic/table/ProjectUsersTable.tsx index 9d278bdc4..27346195c 100644 --- a/frontend/src/components/basic/table/ProjectUsersTable.tsx +++ b/frontend/src/components/basic/table/ProjectUsersTable.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; -import { faX } from '@fortawesome/free-solid-svg-icons'; +import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from '@fortawesome/free-solid-svg-icons'; import { plans } from 'public/data/frequentConstants'; import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; @@ -106,6 +106,11 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => { ability: "read", environmentSlug: slug }]; + } else if (val === "Add Only") { + denials = [{ + ability: "read", + environmentSlug: slug + }]; } else { denials = []; } @@ -185,21 +190,21 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => { return (
-
+
- + {workspaceEnvs.map(env => ( - ))} @@ -221,7 +226,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => { user.email?.toLowerCase().includes(filter) ) .map((row, index) => ( - + @@ -231,7 +236,8 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => { - {workspaceEnvs.map((env) => )}
NAME EMAIL ROLE - {env.name.toUpperCase()}
+
+ {env.slug.toUpperCase()}
{/* PERMISSION */}
{row.firstName} {row.lastName}
+ {workspaceEnvs.map((env) => diff --git a/frontend/src/components/basic/table/UserTable.tsx b/frontend/src/components/basic/table/UserTable.tsx index 02e65c547..a541c0f01 100644 --- a/frontend/src/components/basic/table/UserTable.tsx +++ b/frontend/src/components/basic/table/UserTable.tsx @@ -4,6 +4,7 @@ import { faX } from '@fortawesome/free-solid-svg-icons'; import changeUserRoleInOrganization from '@app/pages/api/organization/changeUserRoleInOrganization'; import deleteUserFromOrganization from '@app/pages/api/organization/deleteUserFromOrganization'; +import getOrganizationProjectMemberships from '@app/pages/api/organization/GetOrgProjectMemberships'; import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace'; import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey'; import uploadKeys from '@app/pages/api/workspace/uploadKeys'; @@ -36,6 +37,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } ); const router = useRouter(); const [myRole, setMyRole] = useState('member'); + const [userProjectMemberships, setUserProjectMemberships] = useState([]); const workspaceId = router.query.id as string; // Delete the row in the table (e.g. a user) @@ -79,6 +81,10 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } useEffect(() => { setMyRole(userData.filter((user) => user.email === myUser)[0]?.role); + (async () => { + const result = await getOrganizationProjectMemberships({ orgId: String(localStorage.getItem("orgData.id"))}) + setUserProjectMemberships(result); + })(); }, [userData, myUser]); const grantAccess = async (id: string, publicKey: string) => { @@ -110,7 +116,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } }; return ( -
+
@@ -118,6 +124,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } + @@ -189,6 +196,17 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg } )} +
NAME EMAIL ROLEPROJECTS
+
+ {userProjectMemberships[row.userId] + ? userProjectMemberships[row.userId]?.map((project: any) => ( +
+ {project.name} +
+ )) + : This user isn't part of any projects yet.} +
+
{myUser !== row.email && // row.role !== "admin" && diff --git a/frontend/src/components/context/Notifications/Notification.tsx b/frontend/src/components/context/Notifications/Notification.tsx index ca1b155bd..921f86dec 100644 --- a/frontend/src/components/context/Notifications/Notification.tsx +++ b/frontend/src/components/context/Notifications/Notification.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import { faX } from '@fortawesome/free-solid-svg-icons'; +import { faXmark } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; type NotificationType = 'success' | 'error' | 'info'; @@ -36,7 +36,7 @@ const Notification = ({ notification, clearNotification }: NotificationProps) => return (
{notification.type === 'error' && ( @@ -48,13 +48,13 @@ const Notification = ({ notification, clearNotification }: NotificationProps) => {notification.type === 'info' && (
)} -

{notification.text}

+

{notification.text}

); diff --git a/frontend/src/components/dashboard/DashboardInputField.tsx b/frontend/src/components/dashboard/DashboardInputField.tsx index a55c3ba61..a5ee0ed3a 100644 --- a/frontend/src/components/dashboard/DashboardInputField.tsx +++ b/frontend/src/components/dashboard/DashboardInputField.tsx @@ -1,9 +1,10 @@ import { memo, SyntheticEvent, useRef } from 'react'; -import { faCircle, faExclamationCircle, faEye, faLayerGroup } from '@fortawesome/free-solid-svg-icons'; +import { faCircle, faCodeBranch, faExclamationCircle, faEye } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import guidGenerator from '../utilities/randomId'; import { HoverObject } from '../v2/HoverCard'; +import { PopoverObject } from '../v2/Popover/Popover'; const REGEX = /([$]{.*?})/g; @@ -112,7 +113,7 @@ const DashboardInputField = ({ }}> @@ -125,24 +126,24 @@ const DashboardInputField = ({ const error = startsWithNumber || isDuplicate; return ( -
-
- onChangeHandler(e.target.value, position)} - type={type} - value={value} - className='z-10 peer ph-no-capture bg-transparent py-2.5 caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200' - spellCheck="false" - placeholder='–' - /> + +
+
+ {value?.split("\n")[0] ? + {value?.split("\n")[0]} + : - } + {value?.split("\n")[1] && + {value?.split("\n")[1]} + } +
-
+ ); } if (type === 'value') { @@ -215,7 +216,7 @@ const DashboardInputField = ({ ))} {value?.split('').length === 0 && EMPTY}
-
+
)} diff --git a/frontend/src/components/dashboard/KeyPair.tsx b/frontend/src/components/dashboard/KeyPair.tsx index 711d25a51..de55b7322 100644 --- a/frontend/src/components/dashboard/KeyPair.tsx +++ b/frontend/src/components/dashboard/KeyPair.tsx @@ -132,7 +132,7 @@ const KeyPair = ({ /> -
+
- { if (deleteRow) { deleteRow({ ids: [keyPair.id], secretName: keyPair?.key }) }}} isPlain /> + :
+
null} + role="button" + tabIndex={0} + onClick={() => { if (deleteRow) { + deleteRow({ ids: [keyPair.id], secretName: keyPair?.key }) + }}} + className="invisible group-hover:visible" + > + +
+
}
diff --git a/frontend/src/components/dashboard/SideBar.tsx b/frontend/src/components/dashboard/SideBar.tsx index 1d0996376..6fac52ed7 100644 --- a/frontend/src/components/dashboard/SideBar.tsx +++ b/frontend/src/components/dashboard/SideBar.tsx @@ -18,7 +18,7 @@ import GenerateSecretMenu from './GenerateSecretMenu'; interface SecretProps { key: string; - value: string; + value: string | undefined; valueOverride: string | undefined; pos: number; id: string; @@ -80,9 +80,9 @@ const SideBar = ({ const { t } = useTranslation(); return ( -
+
{isLoading ? ( -
+
) : ( -
+

{t('dashboard:sidebar.secret')}

)} -
+