diff --git a/.github/workflows/build-staging-img.yml b/.github/workflows/build-staging-img.yml index 3ff23ba23..806e89b87 100644 --- a/.github/workflows/build-staging-img.yml +++ b/.github/workflows/build-staging-img.yml @@ -11,9 +11,9 @@ jobs: - name: 📦 Install dependencies to test all dependencies run: npm ci --only-production working-directory: backend - - name: 🧪 Run tests - run: npm run test:ci - working-directory: backend + # - name: 🧪 Run tests + # run: npm run test:ci + # working-directory: backend - name: Save commit hashes for tag id: commit uses: pr-mpt/actions-commit-hash@v2 diff --git a/backend/Dockerfile b/backend/Dockerfile index 4c3abfcc1..06448ad91 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -17,7 +17,7 @@ WORKDIR /app ENV npm_config_cache /home/node/.npm COPY package*.json ./ -RUN npm ci --only-production +RUN npm ci --only-production && npm cache clean --force COPY --from=build /app . @@ -30,4 +30,4 @@ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ EXPOSE 4000 -CMD ["npm", "run", "start"] +CMD ["node", "build/index.js"] diff --git a/backend/package-lock.json b/backend/package-lock.json index 76b10ce2e..66aeceee2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -37,6 +37,7 @@ "handlebars": "^4.7.7", "helmet": "^5.1.1", "infisical-node": "^1.2.1", + "ioredis": "^5.3.2", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", @@ -3508,8 +3509,7 @@ "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", - "dev": true + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -7031,39 +7031,6 @@ "node": ">=12" } }, - "node_modules/bull/node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/bull/node_modules/ioredis": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", - "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", - "dev": true, - "dependencies": { - "@ioredis/commands": "^1.1.1", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -9071,30 +9038,36 @@ } }, "node_modules/ioredis": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", - "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", "dependencies": { + "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", - "debug": "^4.3.1", - "denque": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", "lodash.defaults": "^4.2.0", - "lodash.flatten": "^4.4.0", "lodash.isarguments": "^3.1.0", - "p-map": "^2.1.0", - "redis-commands": "1.7.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" }, "engines": { - "node": ">=6" + "node": ">=12.22.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/ioredis" } }, + "node_modules/ioredis/node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", @@ -14445,6 +14418,31 @@ "node": ">=10" } }, + "node_modules/probot/node_modules/ioredis": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", + "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", + "dependencies": { + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.1", + "denque": "^1.1.0", + "lodash.defaults": "^4.2.0", + "lodash.flatten": "^4.4.0", + "lodash.isarguments": "^3.1.0", + "p-map": "^2.1.0", + "redis-commands": "1.7.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/probot/node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -19517,8 +19515,7 @@ "@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", - "dev": true + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -22302,31 +22299,6 @@ "msgpackr": "^1.5.2", "semver": "^7.3.2", "uuid": "^8.3.0" - }, - "dependencies": { - "denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "dev": true - }, - "ioredis": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", - "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", - "dev": true, - "requires": { - "@ioredis/commands": "^1.1.1", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - } - } } }, "bytes": { @@ -23809,21 +23781,26 @@ "dev": true }, "ioredis": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", - "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", "requires": { + "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", - "debug": "^4.3.1", - "denque": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", "lodash.defaults": "^4.2.0", - "lodash.flatten": "^4.4.0", "lodash.isarguments": "^3.1.0", - "p-map": "^2.1.0", - "redis-commands": "1.7.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" + }, + "dependencies": { + "denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" + } } }, "ip": { @@ -27791,6 +27768,24 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" }, + "ioredis": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", + "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", + "requires": { + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.1", + "denque": "^1.1.0", + "lodash.defaults": "^4.2.0", + "lodash.flatten": "^4.4.0", + "lodash.isarguments": "^3.1.0", + "p-map": "^2.1.0", + "redis-commands": "1.7.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + } + }, "js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", diff --git a/backend/package.json b/backend/package.json index e9846de54..e8ecb384f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -28,6 +28,7 @@ "handlebars": "^4.7.7", "helmet": "^5.1.1", "infisical-node": "^1.2.1", + "ioredis": "^5.3.2", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index 7ad570885..b022ac5de 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -148,7 +148,12 @@ export const changeMembershipRole = async (req: Request, res: Response) => { const membership = await Membership.findByIdAndUpdate( membershipId, { - role + $set: { + role + }, + $unset: { + customRole: 1 + } }, { new: true diff --git a/backend/src/controllers/v1/organizationController.ts b/backend/src/controllers/v1/organizationController.ts index edd57a3b8..757d1aed7 100644 --- a/backend/src/controllers/v1/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -8,7 +8,7 @@ import { } from "../../models"; import { createOrganization as create } from "../../helpers/organization"; import { addMembershipsOrg } from "../../helpers/membershipOrg"; -import { ACCEPTED, OWNER } from "../../variables"; +import { ACCEPTED, ADMIN } from "../../variables"; import { getLicenseServerUrl, getSiteURL } from "../../config"; import { licenseServerKeyRequest } from "../../config/request"; import { validateRequest } from "../../helpers/validation"; @@ -55,7 +55,7 @@ export const createOrganization = async (req: Request, res: Response) => { await addMembershipsOrg({ userIds: [req.user._id.toString()], organizationId: organization._id.toString(), - roles: [OWNER], + roles: [ADMIN], statuses: [ACCEPTED] }); diff --git a/backend/src/controllers/v2/organizationsController.ts b/backend/src/controllers/v2/organizationsController.ts index 277a29af8..ad776864c 100644 --- a/backend/src/controllers/v2/organizationsController.ts +++ b/backend/src/controllers/v2/organizationsController.ts @@ -159,7 +159,12 @@ export const updateOrganizationMembership = async (req: Request, res: Response) const membership = await MembershipOrg.findByIdAndUpdate( membershipId, { - role + $set: { + role + }, + $unset: { + customRole: 1 + } }, { new: true diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index fad50b0ca..a528436af 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -55,6 +55,9 @@ export const getSecretsRaw = async (req: Request, res: Response) => { secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath; } + if (!environment || !workspaceId) + throw BadRequestError({ message: "Missing environment or workspace id" }); + let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret if (req.user?._id) { const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); diff --git a/backend/src/ee/controllers/v1/roleController.ts b/backend/src/ee/controllers/v1/roleController.ts index 2c87b9ff8..0d5f105ea 100644 --- a/backend/src/ee/controllers/v1/roleController.ts +++ b/backend/src/ee/controllers/v1/roleController.ts @@ -171,18 +171,6 @@ export const getRoles = async (req: Request, res: Response) => { const customRoles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId }); // as this is shared between org and workspace switch the rule set based on it const roles = [ - // owner is only in org level role - ...(isOrgRole - ? [ - { - _id: "owner", - name: "Owner", - slug: "owner", - description: "Complete administration access over the organization.", - permissions: adminPermissions.rules - } - ] - : []), { _id: "admin", name: "Admin", @@ -192,7 +180,7 @@ export const getRoles = async (req: Request, res: Response) => { }, { _id: "member", - name: "Member", + name: isOrgRole ? "Member" : "Developer", slug: "member", description: "Non-administrative role in an organization", permissions: isOrgRole ? memberPermissions.rules : memberProjectPermissions.rules diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 14ea8978d..7c9502656 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -1,6 +1,14 @@ import { Request, Response } from "express"; import { PipelineStage, Types } from "mongoose"; -import { Folder, Membership, Secret, ServiceTokenData, TFolderSchema, User } from "../../../models"; +import { + Folder, + Membership, + Secret, + ServiceTokenData, + TFolderSchema, + User, + Workspace +} from "../../../models"; import { ActorType, AuditLog, @@ -41,6 +49,7 @@ import { getUserProjectPermissions } from "../../services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; +import { BadRequestError } from "../../../utils/errors"; /** * Return secret snapshots for workspace with id [workspaceId] @@ -781,7 +790,10 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => { ProjectPermissionSub.IpAllowList ); - const plan = await EELicenseService.getPlan(req.workspace.organization); + const workspace = await Workspace.findById(workspaceId); + if (!workspace) throw BadRequestError({ message: "Workspace not found" }); + + const plan = await EELicenseService.getPlan(workspace.organization); if (!plan.ipAllowlisting) return res.status(400).send({ @@ -844,7 +856,10 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => { ProjectPermissionSub.IpAllowList ); - const plan = await EELicenseService.getPlan(req.workspace.organization); + const workspace = await Workspace.findById(workspaceId); + if (!workspace) throw BadRequestError({ message: "Workspace not found" }); + + const plan = await EELicenseService.getPlan(workspace.organization); if (!plan.ipAllowlisting) return res.status(400).send({ @@ -933,7 +948,10 @@ export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => { ProjectPermissionSub.IpAllowList ); - const plan = await EELicenseService.getPlan(req.workspace.organization); + const workspace = await Workspace.findById(workspaceId); + if (!workspace) throw BadRequestError({ message: "Workspace not found" }); + + const plan = await EELicenseService.getPlan(workspace.organization); if (!plan.ipAllowlisting) return res.status(400).send({ diff --git a/backend/src/ee/services/ProjectRoleService.ts b/backend/src/ee/services/ProjectRoleService.ts index 26491fab2..51f4a26f1 100644 --- a/backend/src/ee/services/ProjectRoleService.ts +++ b/backend/src/ee/services/ProjectRoleService.ts @@ -158,13 +158,39 @@ const buildMemberPermission = () => { can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); diff --git a/backend/src/ee/services/RoleService.ts b/backend/src/ee/services/RoleService.ts index df5ebf29a..8f8c4f315 100644 --- a/backend/src/ee/services/RoleService.ts +++ b/backend/src/ee/services/RoleService.ts @@ -86,12 +86,17 @@ const buildMemberPermission = () => { can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Read, OrgPermissionSubjects.Member); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Member); can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); + can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); + can(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning); return build({ conditionsMatcher }); }; @@ -114,8 +119,7 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => { throw UnauthorizedRequestError({ message: "User doesn't belong to organization" }); } - if (membership.role === "admin" || membership.role === "owner") - return { permission: adminPermissions, membership }; + if (membership.role === "admin") return { permission: adminPermissions, membership }; if (membership.role === "member") return { permission: memberPermissions, membership }; diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 2775d79c6..803c92aca 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -160,7 +160,7 @@ export const getIntegrationAuthAccessHelper = async ({ let accessId; let accessToken; const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select( - "workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt +refreshCiphertext +refreshIV +refreshTag +accessIdCiphertext +accessIdIV +accessIdTag" + "workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt +refreshCiphertext +refreshIV +refreshTag +accessIdCiphertext +accessIdIV +accessIdTag metadata" ); if (!integrationAuth) diff --git a/backend/src/helpers/signup.ts b/backend/src/helpers/signup.ts index da494f4f7..27b1c16ab 100644 --- a/backend/src/helpers/signup.ts +++ b/backend/src/helpers/signup.ts @@ -1,7 +1,7 @@ import { IUser } from "../models"; import { createOrganization } from "./organization"; import { addMembershipsOrg } from "./membershipOrg"; -import { ACCEPTED, OWNER } from "../variables"; +import { ACCEPTED, ADMIN } from "../variables"; import { sendMail } from "../helpers/nodemailer"; import { TokenService } from "../services"; import { TOKEN_EMAIL_CONFIRMATION } from "../variables"; @@ -14,10 +14,10 @@ import { TOKEN_EMAIL_CONFIRMATION } from "../variables"; * @returns {Boolean} success - whether or not operation was successful */ export const sendEmailVerification = async ({ email }: { email: string }) => { - const token = await TokenService.createToken({ - type: TOKEN_EMAIL_CONFIRMATION, - email, - }); + const token = await TokenService.createToken({ + type: TOKEN_EMAIL_CONFIRMATION, + email + }); // send mail await sendMail({ @@ -25,8 +25,8 @@ export const sendEmailVerification = async ({ email }: { email: string }) => { subjectLine: "Infisical confirmation code", recipients: [email], substitutions: { - code: token, - }, + code: token + } }); }; @@ -36,17 +36,11 @@ export const sendEmailVerification = async ({ email }: { email: string }) => { * @param {String} obj.email - emai * @param {String} obj.code - code that was sent to [email] */ -export const checkEmailVerification = async ({ - email, - code, -}: { - email: string; - code: string; -}) => { +export const checkEmailVerification = async ({ email, code }: { email: string; code: string }) => { await TokenService.validateToken({ type: TOKEN_EMAIL_CONFIRMATION, email, - token: code, + token: code }); }; @@ -58,27 +52,27 @@ export const checkEmailVerification = async ({ * @param {IUser} obj.user - user who we are initializing for */ export const initializeDefaultOrg = async ({ - organizationName, - user, + organizationName, + user }: { - organizationName: string; - user: IUser; + organizationName: string; + user: IUser; }) => { - try { - // create organization with user as owner and initialize a free - // subscription - const organization = await createOrganization({ - email: user.email, - name: organizationName, - }); + try { + // create organization with user as owner and initialize a free + // subscription + const organization = await createOrganization({ + email: user.email, + name: organizationName + }); - await addMembershipsOrg({ - userIds: [user._id.toString()], - organizationId: organization._id.toString(), - roles: [OWNER], - statuses: [ACCEPTED], - }); - } catch (err) { - throw new Error(`Failed to initialize default organization and workspace [err=${err}]`); - } -}; \ No newline at end of file + await addMembershipsOrg({ + userIds: [user._id.toString()], + organizationId: organization._id.toString(), + roles: [ADMIN], + statuses: [ACCEPTED] + }); + } catch (err) { + throw new Error(`Failed to initialize default organization and workspace [err=${err}]`); + } +}; diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index 1eef252b1..cd6cb263f 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -120,6 +120,7 @@ const getApps = async ({ break; case INTEGRATION_GITLAB: apps = await getAppsGitlab({ + integrationAuth, accessToken, teamId, }); @@ -607,6 +608,12 @@ const getAppsLaravelForge = async ({ * @returns {String} apps.name - name of Fly.io apps */ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { + interface FlyioApp { + id: string; + name: string; + hostname: string; + } + const query = ` query($role: String) { apps(type: "container", first: 400, role: $role) { @@ -619,7 +626,7 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { } `; - const res = ( + const res: FlyioApp[] = ( await standardRequest.post( INTEGRATION_FLYIO_API_URL, { @@ -638,8 +645,9 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { ) ).data.data.apps.nodes; - const apps = res.map((a: any) => ({ + const apps = res.map((a: FlyioApp) => ({ name: a.name, + appId: a.id })); return apps; @@ -736,12 +744,16 @@ const getAppsTerraformCloud = async ({ * @returns {String} apps.name - name of GitLab site */ const getAppsGitlab = async ({ + integrationAuth, accessToken, teamId, }: { + integrationAuth: IIntegrationAuth; accessToken: string; teamId?: string; }) => { + const gitLabApiUrl = integrationAuth.url ? `${integrationAuth.url}/api` : INTEGRATION_GITLAB_API_URL; + const apps: App[] = []; let page = 1; @@ -758,7 +770,7 @@ const getAppsGitlab = async ({ }); const { data } = await standardRequest.get( - `${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`, + `${gitLabApiUrl}/v4/groups/${teamId}/projects`, { params, headers: { @@ -785,7 +797,7 @@ const getAppsGitlab = async ({ // case: fetch projects for individual in GitLab const { id } = ( - await standardRequest.get(`${INTEGRATION_GITLAB_API_URL}/v4/user`, { + await standardRequest.get(`${gitLabApiUrl}/v4/user`, { headers: { Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json", @@ -800,7 +812,7 @@ const getAppsGitlab = async ({ }); const { data } = await standardRequest.get( - `${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`, + `${gitLabApiUrl}/v4/users/${id}/projects`, { params, headers: { diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 6605b231f..cb45a9d95 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -328,15 +328,19 @@ const syncSecretsGCPSecretManager = async ({ const pageSize = 100; let pageToken: string | undefined; let hasMorePages = true; + + const filterParam = integration.metadata.secretGCPLabel + ? `?filter=labels.${integration.metadata.secretGCPLabel.labelName}=${integration.metadata.secretGCPLabel.labelValue}` + : ""; while (hasMorePages) { const params = new URLSearchParams({ pageSize: String(pageSize), ...(pageToken ? { pageToken } : {}) }); - + const res: GCPSMListSecretsRes = (await standardRequest.get( - `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets?filter=labels.managed-by=infisical`, + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`, { params, headers: { @@ -347,7 +351,24 @@ const syncSecretsGCPSecretManager = async ({ )).data; if (res.secrets) { - gcpSecrets = gcpSecrets.concat(res.secrets); + const filteredSecrets = res.secrets?.filter((gcpSecret) => { + const arr = gcpSecret.name.split("/"); + const key = arr[arr.length - 1]; + + let isValid = true; + + if (integration.metadata.secretPrefix && !key.startsWith(integration.metadata.secretPrefix)) { + isValid = false; + } + + if (integration.metadata.secretSuffix && !key.endsWith(integration.metadata.secretSuffix)) { + isValid = false; + } + + return isValid; + }); + + gcpSecrets = gcpSecrets.concat(filteredSecrets); } if (!res.nextPageToken) { @@ -371,7 +392,7 @@ const syncSecretsGCPSecretManager = async ({ const key = arr[arr.length - 1]; const secretLatest: GCPLatestSecretVersionAccess = (await standardRequest.get( - `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}/versions/latest:access`, + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`, { headers: { Authorization: `Bearer ${accessToken}`, @@ -379,6 +400,7 @@ const syncSecretsGCPSecretManager = async ({ } } )).data; + res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8"); } @@ -387,14 +409,16 @@ const syncSecretsGCPSecretManager = async ({ if (!(key in res)) { // case: create secret await standardRequest.post( - `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets`, + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets`, { replication: { automatic: {} }, - labels: { - "managed-by": "infisical" - } + ...(integration.metadata.secretGCPLabel ? { + labels: { + [integration.metadata.secretGCPLabel.labelName]: integration.metadata.secretGCPLabel.labelValue + } + } : {}) }, { params: { @@ -408,7 +432,7 @@ const syncSecretsGCPSecretManager = async ({ ); await standardRequest.post( - `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}:addVersion`, + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`, { payload: { data: Buffer.from(secrets[key].value).toString("base64") @@ -428,7 +452,7 @@ const syncSecretsGCPSecretManager = async ({ if (!(key in secrets)) { // case: delete secret await standardRequest.delete( - `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}`, + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}`, { headers: { Authorization: `Bearer ${accessToken}`, @@ -440,7 +464,7 @@ const syncSecretsGCPSecretManager = async ({ // case: update secret if (secrets[key].value !== res[key]) { await standardRequest.post( - `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}:addVersion`, + `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`, { payload: { data: Buffer.from(secrets[key].value).toString("base64") @@ -759,12 +783,12 @@ const syncSecretsAWSParameterStore = async ({ }; /** - * Sync/push [secrets] to AWS secret manager + * Sync/push [secrets] to AWS Secrets Manager * @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.accessId - access id for AWS secret manager integration - * @param {String} obj.accessToken - access token for AWS secret manager integration + * @param {String} obj.accessId - access id for AWS Secrets Manager integration + * @param {String} obj.accessToken - access token for AWS Secrets Manager integration */ const syncSecretsAWSSecretManager = async ({ integration, @@ -1863,10 +1887,24 @@ const syncSecretsGitLab = async ({ }; const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken); - const getSecretsRes: GitLabSecret[] = allEnvVariables.filter( - (secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment - ); + const getSecretsRes: GitLabSecret[] = allEnvVariables + .filter( + (secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment + ) + .filter((gitLabSecret) => { + let isValid = true; + if (integration.metadata.secretPrefix && !gitLabSecret.key.startsWith(integration.metadata.secretPrefix)) { + isValid = false; + } + + if (integration.metadata.secretSuffix && !gitLabSecret.key.endsWith(integration.metadata.secretSuffix)) { + isValid = false; + } + + return isValid; + }); + for await (const key of Object.keys(secrets)) { const existingSecret = getSecretsRes.find((s: any) => s.key == key); if (!existingSecret) { diff --git a/backend/src/integrations/teams.ts b/backend/src/integrations/teams.ts index 3b0564322..46791c5b3 100644 --- a/backend/src/integrations/teams.ts +++ b/backend/src/integrations/teams.ts @@ -34,6 +34,7 @@ const getTeams = async ({ switch (integrationAuth.integration) { case INTEGRATION_GITLAB: teams = await getTeamsGitLab({ + integrationAuth, accessToken, }); break; @@ -51,13 +52,17 @@ const getTeams = async ({ * @returns {String} teams.teamId - id of team */ const getTeamsGitLab = async ({ + integrationAuth, accessToken, }: { + integrationAuth: IIntegrationAuth; accessToken: string; }) => { + const gitLabApiUrl = integrationAuth.url ? `${integrationAuth.url}/api` : INTEGRATION_GITLAB_API_URL; + let teams: Team[] = []; const res = (await standardRequest.get( - `${INTEGRATION_GITLAB_API_URL}/v4/groups`, + `${gitLabApiUrl}/v4/groups`, { headers: { Authorization: `Bearer ${accessToken}`, diff --git a/backend/src/models/integration/integration.ts b/backend/src/models/integration/integration.ts index 7a7775bac..9bc378f28 100644 --- a/backend/src/models/integration/integration.ts +++ b/backend/src/models/integration/integration.ts @@ -187,7 +187,8 @@ const integrationSchema = new Schema( default: "/", }, metadata: { - type: Schema.Types.Mixed + type: Schema.Types.Mixed, + default: {} } }, { diff --git a/backend/src/models/integration/types.ts b/backend/src/models/integration/types.ts index 0415a9556..0c0b998e6 100644 --- a/backend/src/models/integration/types.ts +++ b/backend/src/models/integration/types.ts @@ -1,3 +1,11 @@ + +// TODO: in the future separate metadata +// into distinct types by integration export type Metadata = { + secretPrefix?: string; secretSuffix?: string; + secretGCPLabel?: { + labelName: string; + labelValue: string; + } } \ No newline at end of file diff --git a/backend/src/models/membershipOrg.ts b/backend/src/models/membershipOrg.ts index 530797379..09b16be84 100644 --- a/backend/src/models/membershipOrg.ts +++ b/backend/src/models/membershipOrg.ts @@ -1,5 +1,5 @@ import { Document, Schema, Types, model } from "mongoose"; -import { ACCEPTED, ADMIN, CUSTOM, INVITED, MEMBER, OWNER } from "../variables"; +import { ACCEPTED, ADMIN, CUSTOM, INVITED, MEMBER } from "../variables"; export interface IMembershipOrg extends Document { _id: Types.ObjectId; @@ -26,7 +26,7 @@ const membershipOrgSchema = new Schema( }, role: { type: String, - enum: [OWNER, ADMIN, MEMBER, CUSTOM], + enum: [ADMIN, MEMBER, CUSTOM], required: true }, status: { diff --git a/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts b/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts index d3ffef6e6..7b6819b8c 100644 --- a/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts +++ b/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts @@ -35,9 +35,12 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => { }); const suffixedSecrets: any = {}; - if (integration.metadata?.secretSuffix) { + if (integration.metadata) { for (const key in secrets) { - const newKey = key + integration.metadata?.secretSuffix; + const prefix = (integration.metadata?.secretPrefix || ""); + const suffix = (integration.metadata?.secretSuffix || ""); + const newKey = prefix + key + suffix; + suffixedSecrets[newKey] = secrets[key]; } } diff --git a/backend/src/queues/secret-scanning/githubScanFullRepository.ts b/backend/src/queues/secret-scanning/githubScanFullRepository.ts index 61d53acbb..eeb6aaf1e 100644 --- a/backend/src/queues/secret-scanning/githubScanFullRepository.ts +++ b/backend/src/queues/secret-scanning/githubScanFullRepository.ts @@ -4,7 +4,7 @@ import TelemetryService from "../../services/TelemetryService"; import { sendMail } from "../../helpers"; import GitRisks from "../../ee/models/gitRisks"; import { MembershipOrg, User } from "../../models"; -import { ADMIN, OWNER } from "../../variables"; +import { ADMIN } from "../../variables"; import { convertKeysToLowercase, scanFullRepoContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config"; import { SecretMatch } from "../../ee/services/GithubSecretScanning/types"; @@ -13,7 +13,7 @@ export const githubFullRepositorySecretScan = new Queue("github-full-repository- type TScanPushEventQueueDetails = { organizationId: string, - installationId: number, + installationId: number, repository: { id: number, fullName: string, @@ -24,22 +24,22 @@ githubFullRepositorySecretScan.process(async (job: Job, done: Queue.DoneCallback const { organizationId, repository, installationId }: TScanPushEventQueueDetails = job.data try { const octokit = new ProbotOctokit({ - auth: { + auth: { appId: await getSecretScanningGitAppId(), privateKey: await getSecretScanningPrivateKey(), installationId: installationId - }, + }, }); - const findings : SecretMatch[] = await scanFullRepoContentAndGetFindings(octokit, installationId, repository.fullName) + const findings: SecretMatch[] = await scanFullRepoContentAndGetFindings(octokit, installationId, repository.fullName) for (const finding of findings) { - await GitRisks.findOneAndUpdate({ fingerprint: finding.Fingerprint}, + await GitRisks.findOneAndUpdate({ fingerprint: finding.Fingerprint }, { - ...convertKeysToLowercase(finding), - installationId: installationId, - organization: organizationId, - repositoryFullName: repository.fullName, - repositoryId: repository.id - }, { + ...convertKeysToLowercase(finding), + installationId: installationId, + organization: organizationId, + repositoryFullName: repository.fullName, + repositoryId: repository.id + }, { upsert: true }).lean() } @@ -47,10 +47,7 @@ githubFullRepositorySecretScan.process(async (job: Job, done: Queue.DoneCallback // get emails of admins const adminsOfWork = await MembershipOrg.find({ organization: organizationId, - $or: [ - { role: OWNER }, - { role: ADMIN } - ] + role: ADMIN, }).lean() const userEmails = await User.find({ diff --git a/backend/src/queues/secret-scanning/githubScanPushEvent.ts b/backend/src/queues/secret-scanning/githubScanPushEvent.ts index 71a7e92d4..09261e235 100644 --- a/backend/src/queues/secret-scanning/githubScanPushEvent.ts +++ b/backend/src/queues/secret-scanning/githubScanPushEvent.ts @@ -5,7 +5,7 @@ import TelemetryService from "../../services/TelemetryService"; import { sendMail } from "../../helpers"; import GitRisks from "../../ee/models/gitRisks"; import { MembershipOrg, User } from "../../models"; -import { ADMIN, OWNER } from "../../variables"; +import { ADMIN } from "../../variables"; import { convertKeysToLowercase, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config"; import { SecretMatch } from "../../ee/services/GithubSecretScanning/types"; @@ -88,10 +88,7 @@ githubPushEventSecretScan.process(async (job: Job, done: Queue.DoneCallback) => // get emails of admins const adminsOfWork = await MembershipOrg.find({ organization: organizationId, - $or: [ - { role: OWNER }, - { role: ADMIN } - ] + role: ADMIN }).lean() const userEmails = await User.find({ diff --git a/backend/src/routes/v2/organizations.ts b/backend/src/routes/v2/organizations.ts index a12288886..d1387bd42 100644 --- a/backend/src/routes/v2/organizations.ts +++ b/backend/src/routes/v2/organizations.ts @@ -4,7 +4,7 @@ import { requireAuth, requireOrganizationAuth } from "../../middleware"; -import { ACCEPTED, ADMIN, AuthMode, OWNER } from "../../variables"; +import { ACCEPTED, ADMIN, AuthMode } from "../../variables"; import { organizationsController } from "../../controllers/v2"; // TODO: /POST to create membership @@ -48,7 +48,7 @@ router.get( acceptedAuthModes: [AuthMode.JWT] }), requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], + acceptedRoles: [ADMIN], acceptedStatuses: [ACCEPTED] }), organizationsController.getOrganizationServiceAccounts diff --git a/backend/src/services/RedisService.ts b/backend/src/services/RedisService.ts new file mode 100644 index 000000000..23f945bb7 --- /dev/null +++ b/backend/src/services/RedisService.ts @@ -0,0 +1,12 @@ +import Redis, { Redis as TRedis } from "ioredis"; + +let redisClient: TRedis | null; + +if (process.env.REDIS_URL) { + redisClient = new Redis(process.env.REDIS_URL as string); +} else { + console.warn("Redis URL not set, skipping Redis initialization."); + redisClient = null; +} + +export { redisClient }; diff --git a/backend/src/utils/addDevelopmentUser.ts b/backend/src/utils/addDevelopmentUser.ts index d8b668909..b5b0f3495 100644 --- a/backend/src/utils/addDevelopmentUser.ts +++ b/backend/src/utils/addDevelopmentUser.ts @@ -80,7 +80,7 @@ export const createTestUserForDevelopment = async () => { const testMembershipOrg = { _id: testMembershipOrgId, organization: testOrgId, - role: "owner", + role: "admin", status: "accepted", user: testUserId, } @@ -121,7 +121,7 @@ export const createTestUserForDevelopment = async () => { const workspaceInDB = await Workspace.findById(testWorkspaceId) if (!workspaceInDB) { const workspace = await Workspace.create(testWorkspace) - + // initialize blind index salt for workspace await SecretService.createSecretBlindIndexData({ workspaceId: workspace._id, diff --git a/backend/src/utils/setup/backfillData.ts b/backend/src/utils/setup/backfillData.ts index ae6279bfc..b420761a7 100644 --- a/backend/src/utils/setup/backfillData.ts +++ b/backend/src/utils/setup/backfillData.ts @@ -3,6 +3,7 @@ import crypto from "crypto"; import { Types } from "mongoose"; import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto"; import { EESecretService } from "../../ee/services"; +import { redisClient } from "../../services/RedisService" import { IPType, ISecretVersion, SecretSnapshot, SecretVersion, TrustedIP } from "../../ee/models"; import { AuthMethod, @@ -10,9 +11,11 @@ import { Bot, BotOrg, ISecret, + IWorkspace, Integration, IntegrationAuth, Membership, + MembershipOrg, Organization, Secret, SecretBlindIndexData, @@ -23,13 +26,22 @@ import { import { generateKeyPair } from "../../utils/crypto"; import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; import { + ADMIN, ALGORITHM_AES_256_GCM, + CUSTOM, ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, MEMBER, - VIEWER + OWNER } from "../../variables"; + import { InternalServerError } from "../errors"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + memberProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import Role from "../../ee/models/role"; /** * Backfill secrets to ensure that they're all versioned and have @@ -675,21 +687,135 @@ export const backfillUserAuthMethods = async () => { }; export const backfillPermission = async () => { - await Membership.updateMany( - { - deniedPermissions: { - $exists: true, - $ne: [] - }, - role: MEMBER - }, - [ - { - $set: { - role: VIEWER + const lockKey = "backfill_permission_lock"; + const timeout = 900000; // 15 min lock timeout in milliseconds + const lock = await redisClient?.set(lockKey, 1, "PX", timeout, "NX"); + + if (lock) { + try { + console.info("Lock acquired for script [backfillPermission]"); + + const memberships = await Membership.find({ + deniedPermissions: { + $exists: true, + $ne: [] + }, + role: MEMBER, + }) + .populate<{ workspace: IWorkspace }>("workspace") + .lean(); + + // group memberships that need the same permission set + const roleMap = new Map(); + + for (const membership of memberships) { + // get permissions of members except secret permission + const customPermissions = memberProjectPermissions.rules.filter( + ({ subject }) => subject !== ProjectPermissionSub.Secrets + ); + const secretAccessRule: Record = {}; + + // iterate and record true and false ones + membership.deniedPermissions.forEach(({ ability, environmentSlug }) => { + if (!secretAccessRule?.[environmentSlug]) + secretAccessRule[environmentSlug] = { read: true, write: true }; + if (ability === "write") secretAccessRule[environmentSlug].write = false; + if (ability === "read") secretAccessRule[environmentSlug].read = false; + }); + + // environments that are not listed in deniedPermissions should be set to allowed for both read & and write + membership.workspace.environments.forEach(env => { + if (!secretAccessRule?.[env.slug]) { + secretAccessRule[env.slug] = { read: true, write: true }; + } + }) + + const secretPermissions: any = []; + Object.entries(secretAccessRule).forEach(([envSlug, { read, write }]) => { + if (read) { + secretPermissions.push({ + subject: ProjectPermissionSub.Secrets, + action: ProjectPermissionActions.Read, + conditions: { environment: envSlug } + }); + } + if (write) { + secretPermissions.push( + { + subject: ProjectPermissionSub.Secrets, + action: ProjectPermissionActions.Edit, + conditions: { environment: envSlug } + }, + { + subject: ProjectPermissionSub.Secrets, + action: ProjectPermissionActions.Delete, + conditions: { environment: envSlug } + }, + { + subject: ProjectPermissionSub.Secrets, + action: ProjectPermissionActions.Create, + conditions: { environment: envSlug } + } + ); + } + }); + + const key = `${JSON.stringify(secretPermissions)}-${membership.workspace._id.toString()}`; // group roles that have same permission with in the same workspace + const value = roleMap.get(key); + if (value) { + value.membershipIds.push(membership._id.toString()); + value.organizationId = membership.workspace.organization.toString() + value.workspaceId = membership.workspace._id.toString() + } else { + roleMap.set(key, { membershipIds: [membership._id.toString()], permissions: [...customPermissions, ...secretPermissions], organizationId: membership.workspace.organization.toString(), workspaceId: membership.workspace._id.toString() }); } } - ] - ); - console.log("Backfill: Finishing converting old denied permission in workspace to viewers"); + + for (const [key, value] of roleMap.entries()) { + const { membershipIds, permissions, workspaceId, organizationId } = value + const membership_identity = crypto.randomBytes(3).toString("hex") + const role = new Role({ + name: `Limited [${membership_identity.toUpperCase()}]`, + organization: organizationId, + workspace: workspaceId, + description: "This role was auto generated by Infisical in effort to migrate your project members to our new permission system", + isOrgRole: false, + slug: `custom-role-${membership_identity}`, + permissions: permissions + }); + + await role.save(); + + for (const id of membershipIds) { + await Membership.findByIdAndUpdate(id, { // document db doesn't support update many so we must loop + $set: { + role: CUSTOM, + customRole: role + } + }); + } + } + + console.info("Backfill: Finished converting old denied permission in workspace to viewers"); + + await MembershipOrg.updateMany( + { + role: OWNER + }, + { + $set: { + role: ADMIN + } + } + ); + + console.info("Backfill: Finished converting owner role to member"); + + } catch (error) { + console.error("An error occurred when running script [backfillPermission]:", error); + } + + } else { + console.info("Could not acquire lock for script [backfillPermission], skipping"); + } }; diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index 7ebdd2563..c4e06ff2c 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -84,7 +84,7 @@ export const setup = async () => { await backfillServiceTokenMultiScope(); await backfillTrustedIps(); await backfillUserAuthMethods(); - await backfillPermission(); + // await backfillPermission(); // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY diff --git a/backend/src/validation/integration.ts b/backend/src/validation/integration.ts index 7ec0d0875..7795b0084 100644 --- a/backend/src/validation/integration.ts +++ b/backend/src/validation/integration.ts @@ -34,9 +34,9 @@ export const validateClientForIntegration = async ({ if (!integration) throw IntegrationNotFoundError(); const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth).select( - "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt" + "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt metadata" ); - + if (!integrationAuth) throw IntegrationAuthNotFoundError(); const accessToken = ( @@ -77,7 +77,12 @@ export const CreateIntegrationV1 = z.object({ path: z.string().trim().optional(), region: z.string().trim().optional(), metadata: z.object({ - secretSuffix: z.string().optional() + secretPrefix: z.string().optional(), + secretSuffix: z.string().optional(), + secretGCPLabel: z.object({ + labelName: z.string(), + labelValue: z.string() + }).optional() }).optional() }) }); diff --git a/backend/src/validation/integrationAuth.ts b/backend/src/validation/integrationAuth.ts index 18ff191af..cf3f936a9 100644 --- a/backend/src/validation/integrationAuth.ts +++ b/backend/src/validation/integrationAuth.ts @@ -30,9 +30,9 @@ const validateClientForIntegrationAuth = async ({ const integrationAuth = await IntegrationAuth.findById(integrationAuthId) .populate<{ workspace: IWorkspace }>("workspace") .select( - "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt" + "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt metadata" ); - + if (!integrationAuth) throw IntegrationAuthNotFoundError(); let accessToken, accessId; diff --git a/backend/src/validation/secrets.ts b/backend/src/validation/secrets.ts index 700a3ea5d..28e7f469d 100644 --- a/backend/src/validation/secrets.ts +++ b/backend/src/validation/secrets.ts @@ -225,8 +225,8 @@ export const GetSecretsV2 = z.object({ export const GetSecretsRawV3 = z.object({ query: z.object({ - workspaceId: z.string().trim(), - environment: z.string().trim(), + workspaceId: z.string().trim().optional(), + environment: z.string().trim().optional(), secretPath: z.string().trim().default("/"), folderId: z.string().trim().optional(), include_imports: z @@ -270,7 +270,6 @@ export const UpdateSecretByNameRawV3 = z.object({ workspaceId: z.string().trim(), environment: z.string().trim(), secretValue: z.string().trim(), - secretName: z.string().trim(), secretPath: z.string().trim().default("/"), type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED) }) diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index fe04b6401..e8bf78b19 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -192,7 +192,7 @@ export const getIntegrationOptions = async () => { docsLink: "", }, { - name: "AWS Secret Manager", + name: "AWS Secrets Manager", slug: "aws-secret-manager", image: "Amazon Web Services.png", isAvailable: true, diff --git a/backend/src/variables/organization.ts b/backend/src/variables/organization.ts index bccabc6e6..5e796e357 100644 --- a/backend/src/variables/organization.ts +++ b/backend/src/variables/organization.ts @@ -1,5 +1,5 @@ // membership roles -export const OWNER = "owner"; +export const OWNER = "owner"; // depreciated export const ADMIN = "admin"; export const MEMBER = "member"; export const VIEWER = "viewer"; diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 0e42eab8e..672beefcd 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -402,7 +402,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse path flag") + } + + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}) if err != nil { util.HandleError(err, "To fetch all secrets") } @@ -651,10 +656,11 @@ func init() { secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") secretsCmd.AddCommand(secretsGetCmd) + secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path") secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") secretsCmd.AddCommand(secretsSetCmd) - secretsSetCmd.Flags().String("path", "/", "get secrets within a folder path") + secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path") secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { util.RequireLogin() diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index e0eaa084e..a8d248a3b 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -220,11 +220,11 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models workspaceFile.WorkspaceId = params.WorkspaceId } - // Verify environment - err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials) - if err != nil { - return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err) - } + // // Verify environment + // err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials) + // if err != nil { + // return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err) + // } secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport) @@ -253,32 +253,32 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models return secretsToReturn, errorToReturn } -func ValidateEnvironmentName(environmentName string, workspaceId string, userLoggedInDetails models.UserCredentials) error { - httpClient := resty.New() - httpClient.SetAuthToken(userLoggedInDetails.JTWToken). - SetHeader("Accept", "application/json") +// func ValidateEnvironmentName(environmentName string, workspaceId string, userLoggedInDetails models.UserCredentials) error { +// httpClient := resty.New() +// httpClient.SetAuthToken(userLoggedInDetails.JTWToken). +// SetHeader("Accept", "application/json") - response, err := api.CallGetAccessibleEnvironments(httpClient, api.GetAccessibleEnvironmentsRequest{WorkspaceId: workspaceId}) - if err != nil { - return err - } +// response, err := api.CallGetAccessibleEnvironments(httpClient, api.GetAccessibleEnvironmentsRequest{WorkspaceId: workspaceId}) +// if err != nil { +// return err +// } - listOfEnvSlugs := []string{} - mapOfEnvSlugs := make(map[string]interface{}) +// listOfEnvSlugs := []string{} +// mapOfEnvSlugs := make(map[string]interface{}) - for _, environment := range response.AccessibleEnvironments { - listOfEnvSlugs = append(listOfEnvSlugs, environment.Slug) - mapOfEnvSlugs[environment.Slug] = environment - } +// for _, environment := range response.AccessibleEnvironments { +// listOfEnvSlugs = append(listOfEnvSlugs, environment.Slug) +// mapOfEnvSlugs[environment.Slug] = environment +// } - _, exists := mapOfEnvSlugs[environmentName] - if !exists { - HandleError(fmt.Errorf("the environment [%s] does not exist in project with [id=%s]. Only [%s] are available", environmentName, workspaceId, strings.Join(listOfEnvSlugs, ","))) - } +// _, exists := mapOfEnvSlugs[environmentName] +// if !exists { +// HandleError(fmt.Errorf("the environment [%s] does not exist in project with [id=%s]. Only [%s] are available", environmentName, workspaceId, strings.Join(listOfEnvSlugs, ","))) +// } - return nil +// return nil -} +// } func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string { if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found { diff --git a/docs/changelog/overview.mdx b/docs/changelog/overview.mdx index 4961ef274..bdf820ad2 100644 --- a/docs/changelog/overview.mdx +++ b/docs/changelog/overview.mdx @@ -76,7 +76,7 @@ The changelog below reflects new product developments and updates on a monthly b - Added secret backups support for the CLI; it now fetches and caches secrets locally to be used in the event of future failed fetch. - Added support for comparing secret values across environments on each secret. - Added native AWS Parameter Store integration. -- Added native AWS Secret Manager integration. +- Added native AWS Secrets Manager integration. - Added native GitLab integration. - Added native CircleCI integration. - Added native Travis CI integration. diff --git a/docs/contributing/faq.mdx b/docs/contributing/faq.mdx new file mode 100644 index 000000000..31863687f --- /dev/null +++ b/docs/contributing/faq.mdx @@ -0,0 +1,93 @@ +--- +title: "FAQ" +description: "Frequently Asked Questions about contributing to Infisical" +--- + +Frequently asked questions about contributing to Infisical can be found on this page. +If you can't find the answer you are looking for, please create an issue on our GitHub repository or join our Slack channel for additional support. + + +The Alpine Linux CDN may be unavailable/down in your region infrequently (eg. there is an unplanned outage). One possible fix is to add a retry mechanism and a fallback mirrors array to the Dockerfile. You can also use this as an opportunity to pin the Alpine Linux version for Docker to use in case there are issues with the latest version. Ensure to use https for the mirrors. + +#### Make the following changes to the backend Dockerfile +```bash +# Pin Alpine version from list: https://dl-cdn.alpinelinux.org/alpine/ +ARG ALPINE_VERSION=3.17 +ARG ALPINE_APPEND=v3.17/main + +# Specify number of retries for each mirror +ARG MAX_RETRIES=3 + +# Define base Alpine mirror URLs in attempt order from list: https://dl-cdn.alpinelinux.org/alpine/MIRRORS.txt +ARG BASE_ALPINE_MIRRORS="https://dl-cdn.alpinelinux.org/alpine https://ftp.halifax.rwth-aachen.de/alpine https://uk.alpinelinux.org/alpine" + +# Build stage +# Add the Alpine version arg +FROM node:16-alpine$ALPINE_VERSION AS build + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci --only-production + +COPY . . +RUN npm run build + +# Production stage +# Add the Alpine version arg +FROM node:16-alpine$ALPINE_VERSION + +WORKDIR /app + +ENV npm_config_cache /home/node/.npm + +COPY package*.json ./ +RUN npm ci --only-production + +COPY --from=build /app . + +# Add retry mechanism and loop through the specified mirrors +RUN retries_left=$MAX_RETRIES; \ + for mirror in $ALPINE_MIRRORS; do \ + full_mirror="$mirror/$ALPINE_APPEND"; \ + echo "Trying mirror: $full_mirror"; \ + echo >>/etc/apk/repositories "$full_mirror"; \ + for i in $(seq $retries_left); do \ + echo "Retrying... Attempt $i (Retries Left: $((retries_left - i)))"; \ + if apk add --no-cache bash curl git && \ + curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash && \ + apk add --no-cache infisical=0.8.1; then \ + break; \ + fi; \ + sleep 10; \ + done; \ + if [ $? -eq 0 ]; then \ + break; \ + fi; \ + done + +HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ + CMD node healthcheck.js + +EXPOSE 4000 + +CMD ["npm", "run", "start"] + ``` + + + [Alpine Linux (mirrors) - official site](https://dl-cdn.alpinelinux.org/alpine/MIRRORS.txt) + + + + [Alpine Linux (mirrors) - archived site](https://web.archive.org/web/20230914123159/https://dl-cdn.alpinelinux.org/alpine/MIRRORS.txt) + + + + [Alpine Linux (versions) - official site](https://dl-cdn.alpinelinux.org/alpine/) + + + + [Alpine Linux (versions) - archived site](https://web.archive.org/web/20230914123455/https://dl-cdn.alpinelinux.org/alpine/) + + + diff --git a/docs/documentation/platform/secret-reference.mdx b/docs/documentation/platform/secret-reference.mdx index 380facd94..d0d750330 100644 --- a/docs/documentation/platform/secret-reference.mdx +++ b/docs/documentation/platform/secret-reference.mdx @@ -1,10 +1,9 @@ --- -title: "Reference/Import Secrets" +title: "Reference and Import Secrets" description: "How to use reference secrets in Infisical" --- -Secret referencing is a powerful feature that allows you to create a secret whose value is linked to one or more other secrets. -This is useful when you need to use a single secret's value across multiple other secrets. +Secret referencing is a powerful feature that allows you to values of other secrets. This way, you just need to update the secret value once for it to be propagated to all the references. Consider a scenario where you have a database password. In order to utilize this password, you may need to incorporate it into a database connection string. With secret referencing, you can easily construct these more intricate secrets by directly referencing the base secret. @@ -34,7 +33,7 @@ For instance, to access a secret 'A' composed of secrets 'B' and 'C' from differ When using [service tokens](./token) to fetch referenced secrets, ensure the service token has read access to all referenced environments and folders. Without proper permissions, the final secret value may be incomplete. -## Import entire folders +## Import entire folders/environments While secret referencing effectively minimizes duplication, there might be instances where you need to import or replicate an entire folder's secrets into another. This can be achieved using the 'Import' feature. @@ -50,3 +49,5 @@ Additionally, any secrets you define directly in your environment will override You can modify the order of folders to control overrides using the `Change Order` drag handle. ![secret import change order](../../images/secret-import-change-order.png) + + diff --git a/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create-options.png b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create-options.png new file mode 100644 index 000000000..d084ebb6a Binary files /dev/null and b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create-options.png differ diff --git a/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png index 9e1719a59..afc4724cb 100644 Binary files a/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png and b/docs/images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png differ diff --git a/docs/images/integrations/gitlab/integrations-gitlab-create-options.png b/docs/images/integrations/gitlab/integrations-gitlab-create-options.png new file mode 100644 index 000000000..abb507997 Binary files /dev/null and b/docs/images/integrations/gitlab/integrations-gitlab-create-options.png differ diff --git a/docs/images/integrations/gitlab/integrations-gitlab-create.png b/docs/images/integrations/gitlab/integrations-gitlab-create.png index 4d13658c7..ae3ca7141 100644 Binary files a/docs/images/integrations/gitlab/integrations-gitlab-create.png and b/docs/images/integrations/gitlab/integrations-gitlab-create.png differ diff --git a/docs/integrations/cicd/gitlab.mdx b/docs/integrations/cicd/gitlab.mdx index d7b2637c6..9c3df9720 100644 --- a/docs/integrations/cicd/gitlab.mdx +++ b/docs/integrations/cicd/gitlab.mdx @@ -32,6 +32,16 @@ Press on the GitLab tile and grant Infisical access to your GitLab account. Select which Infisical environment secrets you want to sync to which GitLab repository and press create integration to start syncing secrets to GitLab. ![integrations gitlab](../../images/integrations/gitlab/integrations-gitlab-create.png) + +Note that the GitLab integration supports a few options in the **Options** tab: + +- Secret Prefix: If inputted, the prefix is appended to the front of every secret name prior to being synced. +- Secret Suffix: If inputted, the suffix to appended to the back of every name of every secret prior to being synced. + +Setting a secret prefix or suffix ensures that existing secrets in GCP Secret Manager are not overwritten during the sync. As part of this process, Infisical abstains from mutating any secrets in GitLab without the specified prefix or suffix. + +![integrations gitlab options](../../images/integrations/gitlab/integrations-gitlab-create-options.png) + ![integrations gitlab](../../images/integrations/gitlab/integrations-gitlab.png) diff --git a/docs/integrations/cloud/aws-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx index a798ae820..4df054077 100644 --- a/docs/integrations/cloud/aws-secret-manager.mdx +++ b/docs/integrations/cloud/aws-secret-manager.mdx @@ -1,6 +1,6 @@ --- -title: "AWS Secret Manager" -description: "How to sync secrets from Infisical to AWS Secret Manager" +title: "AWS Secrets Manager" +description: "How to sync secrets from Infisical to AWS Secrets Manager" --- Prerequisites: @@ -8,15 +8,15 @@ Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - Set up AWS and have/create an IAM user -## Grant the IAM user permissions to access AWS Secret Manager +## Grant the IAM user permissions to access AWS Secrets Manager -Navigate to your IAM user permissions and add a permission policy to grant access to AWS Secret Manager. +Navigate to your IAM user permissions and add a permission policy to grant access to AWS Secrets Manager. ![integration IAM 1](../../images/integrations-aws-iam-1.png) ![integration IAM 2](../../images/integrations-aws-secret-manager-iam-2.png) ![integrations IAM 3](../../images/integrations-aws-secret-manager-iam-3.png) -For better security, here's a custom policy containing the minimum permissions required by Infisical to sync secrets to AWS Secret Manager for the IAM user that you can use: +For better security, here's a custom policy containing the minimum permissions required by Infisical to sync secrets to AWS Secrets Manager for the IAM user that you can use: ```json { @@ -40,7 +40,7 @@ For better security, here's a custom policy containing the minimum permissions r ![integrations](../../images/integrations.png) -## Authorize Infisical for AWS Secret Manager +## Authorize Infisical for AWS Secrets Manager Obtain a AWS access key ID and secret access key for your IAM user in IAM > Users > User > Security credentials > Access keys @@ -48,7 +48,7 @@ Obtain a AWS access key ID and secret access key for your IAM user in IAM > User ![access key 2](../../images/integrations-aws-access-key-2.png) ![access key 3](../../images/integrations-aws-access-key-3.png) -Press on the AWS Secret Manager tile and input your AWS access key ID and secret access key from the previous step. +Press on the AWS Secrets Manager tile and input your AWS access key ID and secret access key from the previous step. ![integration auth](../../images/integrations-aws-secret-manager-auth.png) @@ -61,12 +61,12 @@ Press on the AWS Secret Manager tile and input your AWS access key ID and secret ## Start integration -Select which Infisical environment secrets you want to sync to which AWS Secret Manager region and under which secret name. Then, press create integration to start syncing secrets to AWS Secret Manager. +Select which Infisical environment secrets you want to sync to which AWS Secrets Manager region and under which secret name. Then, press create integration to start syncing secrets to AWS Secrets Manager. ![integration create](../../images/integrations-aws-secret-manager-create.png) - Infisical currently syncs environment variables to AWS Secret Manager as + Infisical currently syncs environment variables to AWS Secrets Manager as key-value pairs under one secret. We're actively exploring ways to help users group environment variable key-pairs under multiple secrets for greater control. diff --git a/docs/integrations/cloud/gcp-secret-manager.mdx b/docs/integrations/cloud/gcp-secret-manager.mdx index 3c1863006..505475009 100644 --- a/docs/integrations/cloud/gcp-secret-manager.mdx +++ b/docs/integrations/cloud/gcp-secret-manager.mdx @@ -35,14 +35,21 @@ Grant Infisical access to GCP. ## Start integration -Select which Infisical environment secrets you want to sync to which GCP secret manager project. Lastly, press create integration to start syncing secrets to GCP secret manager. +In the **Connection** tab, select which Infisical environment secrets you want to sync to which GCP secret manager project. Lastly, press create integration to start syncing secrets to GCP secret manager. ![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png) -![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png) - - Secrets synced from Infisical to GCP Secret Manager are automatically labeled `managed-by:infisical` to avoid overwriting existing values in GCP Secret Manager. - +Note that the GCP Secret Manager integration supports a few options in the **Options** tab: + +- Secret Prefix: If inputted, the prefix is appended to the front of every secret name prior to being synced. +- Secret Suffix: If inputted, the suffix to appended to the back of every name of every secret prior to being synced. +- Label in GCP Secret Manager: If selected, every secret will be labeled in GCP Secret Manager (e.g. as `managed-by:infisical`); labels can be customized. + +Setting a secret prefix, suffix, or enabling the labeling option ensures that existing secrets in GCP Secret Manager are not overwritten during the sync. As part of this process, Infisical abstains from mutating any secrets in GCP Secret Manager without the specified prefix, suffix, or attached label. + +![integrations GCP secret manager options](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create-options.png) + +![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png) Using Infisical to sync secrets to GCP Secret Manager requires that you enable @@ -89,14 +96,21 @@ service account in IAM & Admin > Service Accounts > Service Account > Keys). ## Start integration -Select which Infisical environment secrets you want to sync to the GCP secret manager project. Lastly, press create integration to start syncing secrets to GCP secret manager. +In the **Connection** tab, select which Infisical environment secrets you want to sync to the GCP secret manager project. Lastly, press create integration to start syncing secrets to GCP secret manager. ![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png) -![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png) - - Secrets synced from Infisical to GCP Secret Manager are automatically labeled `managed-by:infisical` to avoid overwriting existing values in GCP Secret Manager. - +Note that the GCP Secret Manager integration supports a few options in the **Options** tab: + +- Secret Prefix: If inputted, the prefix is appended to the front of every secret name prior to being synced. +- Secret Suffix: If inputted, the suffix to appended to the back of every name of every secret prior to being synced. +- Label in GCP Secret Manager: If selected, every secret will be labeled in GCP Secret Manager (e.g. as `managed-by:infisical`); labels can be customized. + +Setting a secret prefix, suffix, or enabling the labeling option ensures that existing secrets in GCP Secret Manager are not overwritten during the sync. As part of this process, Infisical abstains from mutating any secrets in GCP Secret Manager without the specified prefix, suffix, or attached label. + +![integrations GCP secret manager options](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create-options.png) + +![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png) Using Infisical to sync secrets to GCP Secret Manager requires that you enable diff --git a/docs/integrations/cloud/windmill.mdx b/docs/integrations/cloud/windmill.mdx index 594d483b0..c79931b70 100644 --- a/docs/integrations/cloud/windmill.mdx +++ b/docs/integrations/cloud/windmill.mdx @@ -13,7 +13,7 @@ Prerequisites: ## Enter your Windmill Access Token -Obtain a Windmill access token in Access Tokens +Obtain a [Windmill](https://www.windmill.dev/) access token in Access Tokens ![integrations windmill dashboard](../../images/integrations-windmill-dashboard.png) ![integrations windmill token](../../images/integrations-windmill-token.png) diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index a464aa9cb..fc05d817c 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -29,7 +29,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi | [Checkly](/integrations/cloud/checkly) | Cloud | Available | | [HashiCorp Vault](/integrations/cloud/hashicorp-vault) | Cloud | Available | | [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available | -| [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available | +| [AWS Secrets Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available | | [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available | | [GCP Secret Manager](/integrations/cloud/gcp-secret-manager) | Cloud | Available | | [Windmill](/integrations/cloud/windmill) | Cloud | Available | diff --git a/docs/mint.json b/docs/mint.json index 8c39483ea..9eaeddb1f 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -396,7 +396,8 @@ "contributing/overview", "contributing/code-of-conduct", "contributing/developing", - "contributing/pull-requests" + "contributing/pull-requests", + "contributing/faq" ] } ], diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 11adf2e7a..7425cb994 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -5,7 +5,7 @@ interface Mapping { const integrationSlugNameMapping: Mapping = { "azure-key-vault": "Azure Key Vault", "aws-parameter-store": "AWS Parameter Store", - "aws-secret-manager": "AWS Secret Manager", + "aws-secret-manager": "AWS Secrets Manager", "heroku": "Heroku", "vercel": "Vercel", "netlify": "Netlify", diff --git a/frontend/src/components/utilities/parseDotEnv.ts b/frontend/src/components/utilities/parseDotEnv.ts index 683670b33..bec3a506b 100644 --- a/frontend/src/components/utilities/parseDotEnv.ts +++ b/frontend/src/components/utilities/parseDotEnv.ts @@ -1,5 +1,5 @@ const LINE = - /(?:^|^)\s*(?:export\s+)?([\w.-:]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; + /(?:^|^)\s*(?:export\s+)?([\w.:-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; /** * Return text that is the buffer parsed diff --git a/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx b/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx index 732466cf6..bb7fab7ec 100644 --- a/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx +++ b/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx @@ -16,14 +16,6 @@ export const ProjectPermissionProvider = ({ children }: Props): JSX.Element => { const workspaceId = currentWorkspace?._id || ""; const { data: permission, isLoading } = useGetUserProjectPermissions({ workspaceId }); - if (!permission && currentWorkspace) { - return ( -
- Failed to load user permissions -
- ); - } - if ((isLoading && currentWorkspace) || isWsLoading) { return (
@@ -37,6 +29,14 @@ export const ProjectPermissionProvider = ({ children }: Props): JSX.Element => { ); } + if (!permission && currentWorkspace) { + return ( +
+ Failed to load user permissions +
+ ); + } + return ( {children} diff --git a/frontend/src/hoc/withPermission/withPermission.tsx b/frontend/src/hoc/withPermission/withPermission.tsx index 870c54f81..e0c402af3 100644 --- a/frontend/src/hoc/withPermission/withPermission.tsx +++ b/frontend/src/hoc/withPermission/withPermission.tsx @@ -45,8 +45,7 @@ export const withPermission = (
Access Restricted
- Your role has limited permissions, please
You do not have permission to this - page. + Your role has limited permissions, please
contact your admin to gain access
diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 8857da834..de5aa6da0 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -57,6 +57,7 @@ export const useCreateIntegration = () => { path?: string; region?: string; metadata?: { + secretPrefix?: string; secretSuffix?: string; } }) => { diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index d03161f30..8aa7d01b7 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -138,7 +138,6 @@ export const AppLayout = ({ children }: LayoutProps) => { const { t } = useTranslation(); - const logout = useLogoutUser(); const logOutUser = async () => { try { @@ -446,7 +445,7 @@ export const AppLayout = ({ children }: LayoutProps) => { @@ -469,7 +468,7 @@ export const AppLayout = ({ children }: LayoutProps) => { {t("nav.menu.integrations")} @@ -488,7 +487,7 @@ export const AppLayout = ({ children }: LayoutProps) => { - + {/* { Audit Logs - - {/* - - - Audit Logs - - */} - {/* - - } - icon="system-outline-82-extension" - > - Audit Logs - - - */} - - AWS Parameter Store Integration + + Authorize AWS Parameter Integration + + + + + + +
+
+
Pro Tips
+ After creating an integration, your secrets will start syncing immediately. This might cause an unexpected override of current secrets in AWS Parameter Store with secrets from Infisical. +
) : ( -
+
+ + Set Up AWS Parameter Store Integration + + + {isintegrationAuthLoading ? infisical loading indicator :
+ +

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

+
} +
); } diff --git a/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx b/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx index f150a5a28..f69d46f71 100644 --- a/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx +++ b/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx @@ -1,5 +1,10 @@ import { useState } from "react"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; import { useRouter } from "next/router"; +import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; @@ -54,12 +59,41 @@ export default function AWSSecretManagerCreateIntegrationPage() { return (
- - AWS Secret Manager Integration + + Authorize AWS Secrets Manager Integration + + + + +
+
+ AWS logo +
+ AWS Secrets Manager Integration + + +
+ + Docs + +
+
+ +
+
setAccessKey(e.target.value)} /> @@ -67,6 +101,7 @@ export default function AWSSecretManagerCreateIntegrationPage() { label="Secret Access Key" errorText={accessSecretKeyErrorText} isError={accessSecretKeyErrorText !== "" ?? false} + className="px-6" >
diff --git a/frontend/src/pages/integrations/aws-secret-manager/create.tsx b/frontend/src/pages/integrations/aws-secret-manager/create.tsx index 99b89b0fb..3c5b20511 100644 --- a/frontend/src/pages/integrations/aws-secret-manager/create.tsx +++ b/frontend/src/pages/integrations/aws-secret-manager/create.tsx @@ -1,5 +1,10 @@ import { useEffect, useState } from "react"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; import { useRouter } from "next/router"; +import { faArrowUpRightFromSquare, faBookOpen, faBugs, faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import queryString from "query-string"; import { @@ -57,7 +62,7 @@ export default function AWSSecretManagerCreateIntegrationPage() { const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); - const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); + const { data: integrationAuth, isLoading: isintegrationAuthLoading } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); const [secretPath, setSecretPath] = useState("/"); @@ -112,10 +117,38 @@ export default function AWSSecretManagerCreateIntegrationPage() { }; return integrationAuth && workspace && selectedSourceEnvironment ? ( -
- - AWS Secret Manager Integration - +
+ + Set Up AWS Secrets Manager Integration + + + + +
+
+ AWS logo +
+ AWS Secrets Manager Integration + + +
+ + Docs + +
+
+ +
+
+ - + setSecretPath(evt.target.value)} placeholder="Provide a path, default is /" /> - + Create Integration
+
+
+
Pro Tips
+ After creating an integration, your secrets will start syncing immediately. This might cause an unexpected override of current secrets in AWS Secrets Manager with secrets from Infisical. +
) : ( -
+
+ + Set Up AWS Secrets Manager Integration + + + {isintegrationAuthLoading ? infisical loading indicator :
+ +

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

+
} +
); } diff --git a/frontend/src/pages/integrations/checkly/authorize.tsx b/frontend/src/pages/integrations/checkly/authorize.tsx index a84c9fe28..c3938641b 100644 --- a/frontend/src/pages/integrations/checkly/authorize.tsx +++ b/frontend/src/pages/integrations/checkly/authorize.tsx @@ -1,5 +1,10 @@ import { useState } from "react"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; import { useRouter } from "next/router"; +import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useSaveIntegrationAccessToken @@ -41,8 +46,36 @@ export default function ChecklyCreateIntegrationPage() { return (
+ + Authorize Checkly Integration + + - Checkly Integration + +
+
+ Checkly logo +
+ Checkly Integration + + +
+ + Docs + +
+
+ +
+
+
+
+
Pro Tips
+ After creating an integration, your secrets will start syncing immediately. This might cause an unexpected override of current secrets in Checkly with secrets from Infisical. + If you have multiple Checkly integrations and are using suffixes for at least one of them, you will have to add suffixes for all the active Checkly integrations – otherwise you might run into rare unexpected behavior. +
) : ( -
+
+ + Set Up Checkly Integration + + + {isIntegrationAuthAppsLoading ? infisical loading indicator :
+ +

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

+
} +
); } diff --git a/frontend/src/pages/integrations/circleci/authorize.tsx b/frontend/src/pages/integrations/circleci/authorize.tsx index 592056bbe..f276362f7 100644 --- a/frontend/src/pages/integrations/circleci/authorize.tsx +++ b/frontend/src/pages/integrations/circleci/authorize.tsx @@ -1,5 +1,10 @@ import { useState } from "react"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; import { useRouter } from "next/router"; +import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useSaveIntegrationAccessToken @@ -41,19 +46,49 @@ export default function CircleCICreateIntegrationPage() { return (
- - CircleCI Integration + + Authorize CircleCI Integration + + + + +
+
+ CircleCI logo +
+ CircleCI Integration + + +
+ + Docs + +
+
+ +
+
setApiKey(e.target.value)} /> + +
); } -FlyioCreateIntegrationPage.requireAuth = true; +FlyioAuthorizeIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/flyio/create.tsx b/frontend/src/pages/integrations/flyio/create.tsx index 8a49d4c06..be1a577de 100644 --- a/frontend/src/pages/integrations/flyio/create.tsx +++ b/frontend/src/pages/integrations/flyio/create.tsx @@ -1,5 +1,10 @@ import { useEffect, useState } from "react"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; import { useRouter } from "next/router"; +import { faArrowUpRightFromSquare, faBookOpen, faBugs, faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import queryString from "query-string"; import { @@ -23,13 +28,14 @@ import { useGetWorkspaceById } from "../../../hooks/api/workspace"; export default function FlyioCreateIntegrationPage() { const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); - const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); - const { data: integrationAuthApps } = useGetIntegrationAuthApps({ + const { data: integrationAuth, isLoading: isIntegrationAuthLoading } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); + const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } = useGetIntegrationAuthApps({ integrationAuthId: (integrationAuthId as string) ?? "" }); @@ -84,10 +90,38 @@ export default function FlyioCreateIntegrationPage() { selectedSourceEnvironment && integrationAuthApps && targetApp ? ( -
- - Fly.io Integration - +
+ + Set Up Fly.io Integration + + + + +
+
+ Fly.io logo +
+ Fly.io Integration + + +
+ + Docs + +
+
+ +
+
+ - + setSecretPath(evt.target.value)} placeholder="Provide a path, default is /" /> - +