1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-28 15:29:21 +00:00

Compare commits

..

73 Commits

Author SHA1 Message Date
6b2eb9c6c9 fix: resolves a false org not logged in error 2024-12-17 14:37:41 +05:30
b669b0a9f8 Merge pull request from Infisical/feat/sync-circle-ci-context
feat: circle ci context integration
2024-12-17 02:12:32 +08:00
9e768640cd misc: made scope project the default 2024-12-17 00:12:25 +08:00
e3d29b637d misc: added type assertion 2024-12-16 22:27:29 +08:00
9cd0dc8970 Merge pull request from akhilmhdh/fix/group-access-failing 2024-12-16 09:25:01 -05:00
f8f5000bad misc: addressed review comments 2024-12-16 22:20:59 +08:00
40919ccf59 misc: finalized docs and other details 2024-12-16 20:15:14 +08:00
=
44303aca6a fix: group only access to project failing 2024-12-16 16:09:05 +05:30
4bd50c3548 misc: unified to a single integration 2024-12-16 16:08:51 +08:00
64a982d5e0 Merge pull request from akhilmhdh/feat/split-project
feat: changed multi insert into batch insert
2024-12-13 14:52:48 -05:00
=
1080438ad8 feat: changed multi insert into batch insert 2024-12-14 01:19:56 +05:30
eb3acae332 Merge pull request from akhilmhdh/feat/split-project
One slice - 3 Projects
2024-12-13 14:36:58 -05:00
=
a0b3520899 feat: updated rollback 2024-12-14 01:00:12 +05:30
2f6f359ddf Merge pull request from Infisical/misc/operator-namespace-installation
feat: k8 operator namespace installation
2024-12-13 14:10:45 -05:00
=
df8c1e54e0 feat: review changes 2024-12-13 23:50:49 +05:30
=
cac060deff feat: added space 2024-12-13 21:38:44 +05:30
=
47269bc95b feat: resolved undefined redirect 2024-12-13 21:38:44 +05:30
=
8502e9a1d8 feat: removed console log 2024-12-13 21:38:43 +05:30
=
d89eb4fa84 feat: added check in workspace cert api 2024-12-13 21:38:43 +05:30
=
ca7ab4eaf1 feat: resolved typo in access control 2024-12-13 21:38:43 +05:30
=
c57fc5e3f1 feat: fixed review comments 2024-12-13 21:38:43 +05:30
=
9b4e1f561e feat: removed service token from migration and resolved failing migration on groups 2024-12-13 21:38:43 +05:30
=
097fcad5ae fix: resolved failing seed 2024-12-13 21:38:43 +05:30
=
d1547564f9 feat: run through check to all frontend urls 2024-12-13 21:38:43 +05:30
=
24acb98978 feat: project settings hiding 2024-12-13 21:38:42 +05:30
=
0fd8274ff0 feat: added project id mapping logic for cert and kms 2024-12-13 21:38:42 +05:30
=
a857375cc1 feat: fixed migration issues and resolved all routes in frontend 2024-12-13 21:38:42 +05:30
=
69bf9dc20f feat: completed migration 2024-12-13 21:38:42 +05:30
=
5151c91760 feat: check for cmek implemented 2024-12-13 21:38:42 +05:30
=
f12d8b6f89 feat: check for cert manager endpoints 2024-12-13 21:38:42 +05:30
=
695c499448 feat: added type for project and validation check for secret manager specific endpoints 2024-12-13 21:38:42 +05:30
1cbf030e6c Merge remote-tracking branch 'origin/main' into feat/sync-circle-ci-context 2024-12-13 22:34:06 +08:00
dc715cc238 Merge pull request from Infisical/misc/address-high-cpu-usage-from-secret-version-query
misc: address cpu usage issue of secret version query
2024-12-13 08:34:36 -05:00
d873f2e50f misc: address cpu usage issue of secret version query 2024-12-13 20:31:34 +08:00
16ea757928 Merge pull request from Infisical/feat/jwt-auth
feat: jwt auth
2024-12-13 14:15:43 +08:00
5b4487fae8 add period to secret share text 2024-12-12 16:04:51 -05:00
474731d8ef update share secret text 2024-12-12 16:02:30 -05:00
e9f254f81b Update azure-devops.mdx 2024-12-12 15:36:38 -05:00
639057415f Merge remote-tracking branch 'origin/main' into misc/operator-namespace-installation 2024-12-13 03:49:10 +08:00
c38dae2319 misc: updated version 2024-12-13 03:06:07 +08:00
25191cff38 Merge pull request from Infisical/maidul-update-make-wish
Update make wish text
2024-12-12 10:05:12 -05:00
a6898717f4 update make wish text 2024-12-12 10:01:13 -05:00
cc77175188 Merge pull request from Infisical/daniel/plain-to-pylon
feat: remove plain and move to pylon
2024-12-11 19:56:56 -05:00
fcb944d964 Merge pull request from Infisical/omar/eng-1806-add-instance-url-to-email-verification-for-infisical
improvement: Add email footer with instance URL
2024-12-11 19:48:27 -05:00
a8ad8707ac Merge pull request from Infisical/daniel/copy-paste
fix(dashboard): pasting secrets into create secret modal
2024-12-12 03:56:43 +04:00
4568370552 Update parseEnvVar.ts 2024-12-12 03:55:27 +04:00
c000a6f707 more requested changes 2024-12-12 03:34:08 +04:00
1ace8eebf8 fix(k8s): dynamic secret bugs 2024-12-12 03:27:07 +04:00
3b3482b280 fix: improve ref handling 2024-12-11 21:51:20 +04:00
422fd27b9a fix: requested changes 2024-12-11 21:44:42 +04:00
ba5e6fe28a Merge pull request from muhammed-mamun/patch-1
Fix typo in README.md
2024-12-11 10:19:17 -05:00
1a55909b73 Fix typo in README.md
Corrected the typo "Cryptograhic" to "Cryptographic" in the README.md file.
2024-12-11 19:59:06 +06:00
c680030f01 Merge pull request from Infisical/misc/moved-integration-auth-to-params
misc: moved integration auth to params
2024-12-11 19:04:39 +08:00
cf1070c65e misc: moved integration auth to params 2024-12-11 17:56:30 +08:00
3a8219db03 fix: requested changes 2024-12-11 08:32:10 +04:00
7c8f2e5548 docs + minor fixes 2024-12-10 21:14:13 +01:00
a730b16318 fix circleCI name spacing 2024-12-10 20:12:55 +01:00
cc3d132f5d feat(integrations): New CircleCI Context Sync 2024-12-10 20:07:23 +01:00
e32716c258 improvement: Better group member management ()
* improvement: Better org member management
2024-12-10 14:10:14 +01:00
7f0d27e3dc Merge pull request from Infisical/daniel/improve-project-creation-speed
fix(dashboard): improved project creation speed
2024-12-10 16:33:39 +04:00
5d9b99bee7 Update NewProjectModal.tsx 2024-12-10 07:47:36 +04:00
8fdc438940 feat: remove plain and move to pylon 2024-12-10 07:32:09 +04:00
d2b909b72b fix(dashboard): pasting secrets into create secret modal 2024-12-10 04:01:17 +04:00
68988a3e78 Merge pull request from Infisical/misc/add-ssl-setting-pg-bpss
misc: add ssl setting for pg boss
2024-12-09 18:11:09 -05:00
3c954ea257 set all instances to show URL 2024-12-09 21:46:56 +01:00
a92de1273e Merge pull request from akhilmhdh/feat/integration-auth-update-endpoint
feat: added endpoint to update integration auth
2024-12-09 14:42:10 -05:00
97f85fa8d9 fix(Approval Workflows): Workflows keep approval history after deletion ()
* improvement: Approval Workflows can be deleted while maintaining history
Co-authored-by: Daniel Hougaard <daniel@infisical.com>
2024-12-09 20:03:45 +01:00
=
a808b6d4a0 feat: added new audit log event in ui 2024-12-09 20:24:30 +05:30
=
826916399b feat: changed integration option to nativeEnum in zod and added audit log event 2024-12-09 20:16:34 +05:30
7d5aba258a improvement: Add email footer with instance URL 2024-12-09 15:16:05 +01:00
=
40d69d4620 feat: added endpoint to update integration auth 2024-12-09 19:15:17 +05:30
3f6b1fe3bd misc: add ssl setting for pg boss 2024-12-09 13:17:04 +08:00
39f71f9488 feat: k8 operator namespace installation 2024-12-05 23:12:37 +08:00
227 changed files with 5797 additions and 2548 deletions
README.md
backend
e2e-test
package-lock.jsonpackage.json
src
@types
db
ee
lib
api-docs
config
main.ts
queue
server/routes
services
docs
frontend
public
images/integrations
lotties
src
components
context/WorkspaceContext
helpers
hooks/api
layouts/AppLayout
pages
cert-manager/[id]
allowlist
ca/[caId]
certificates
identities/[identityId]
members
[membershipId]
index.tsx
pki-collections/[collectionId]
roles/[roleSlug]
settings
integrations/circleci
kms/[id]
allowlist
identities/[identityId]
kms
members
[membershipId]
index.tsx
roles/[roleSlug]
settings
org/[id]
cert-manager
groups/[groupId]
kms
overview
secret-manager
secret-manager/[id]
allowlist
approval
identities/[identityId]
members
[membershipId]
index.tsx
roles/[roleSlug]
secret-rotation
secrets
settings
signup
signupinvite.tsx
views
IntegrationsPage
IntegrationDetailsPage/components
components/IntegrationsSection/components
Login
Org
GroupPage
IdentityPage/components/IdentityProjectsSection
MembersPage/components
OrgGroupsTab/components/OrgGroupsSection
OrgMembersTab/components/OrgMembersSection
UserPage/components/UserProjectsSection
components
OrgAdminPage/components/OrgAdminProjects
Project
CaPage
CertificatesPage/components
CaTab/components
PkiAlertsTab/components
IdentityDetailsPage
MemberDetailsPage
MembersPage
MembersPage.tsx
components
IdentityTab
MembersTab/components
ProjectRoleListTab/components/ProjectRoleList
PkiCollectionPage
RolePage
SecretApprovalPage/components/AccessApprovalRequest
SecretMainPage
SecretMainPage.tsx
components/CreateSecretForm
SecretOverviewPage
SecretOverviewPage.tsx
components
CreateSecretForm
SecretV2MigrationSection
Settings/ProjectSettingsPage
ProjectSettingsPage.tsx
components
DeleteProjectSection
ProjectGeneralTab
ShareSecretPage/components
Signup/components/BackupPDFStep
admin/DashboardPage
helm-charts/secrets-operator
k8-operator

@ -66,7 +66,7 @@ We're on a mission to make security tooling more accessible to everyone, not jus
### Key Management (KMS):
- **[Cryptograhic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API.
- **[Cryptographic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API.
- **[Encrypt and Decrypt Data](https://infisical.com/docs/documentation/platform/kms#guide-to-encrypting-data)**: Use symmetric keys to encrypt and decrypt data.
### General Platform:

@ -53,7 +53,7 @@ export default {
extension: "ts"
});
const smtp = mockSmtpServer();
const queue = queueServiceFactory(cfg.REDIS_URL, cfg.DB_CONNECTION_URI);
const queue = queueServiceFactory(cfg.REDIS_URL, { dbConnectionUrl: cfg.DB_CONNECTION_URI });
const keyStore = keyStoreFactory(cfg.REDIS_URL);
const hsmModule = initializeHsmModule();

@ -49,7 +49,6 @@
"@sindresorhus/slugify": "1.1.0",
"@slack/oauth": "^3.0.1",
"@slack/web-api": "^7.3.4",
"@team-plain/typescript-sdk": "^4.6.1",
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
"argon2": "^0.31.2",
@ -5678,14 +5677,6 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/@graphql-typed-document-node/core": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
"peerDependencies": {
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.2.tgz",
@ -9970,18 +9961,6 @@
"optional": true,
"peer": true
},
"node_modules/@team-plain/typescript-sdk": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/@team-plain/typescript-sdk/-/typescript-sdk-4.6.1.tgz",
"integrity": "sha512-Uy9QJXu9U7bJb6WXL9sArGk7FXPpzdqBd6q8tAF1vexTm8fbTJRqcikTKxGtZmNADt+C2SapH3cApM4oHpO4lQ==",
"dependencies": {
"@graphql-typed-document-node/core": "^3.2.0",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"graphql": "^16.6.0",
"zod": "3.22.4"
}
},
"node_modules/@techteamer/ocsp": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@techteamer/ocsp/-/ocsp-1.0.1.tgz",
@ -15180,14 +15159,6 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
"node_modules/graphql": {
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz",
"integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==",
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
},
"node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",

@ -157,7 +157,6 @@
"@sindresorhus/slugify": "1.1.0",
"@slack/oauth": "^3.0.1",
"@slack/web-api": "^7.3.4",
"@team-plain/typescript-sdk": "^4.6.1",
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
"argon2": "^0.31.2",

@ -202,6 +202,9 @@ import {
TProjectSlackConfigs,
TProjectSlackConfigsInsert,
TProjectSlackConfigsUpdate,
TProjectSplitBackfillIds,
TProjectSplitBackfillIdsInsert,
TProjectSplitBackfillIdsUpdate,
TProjectsUpdate,
TProjectTemplates,
TProjectTemplatesInsert,
@ -838,5 +841,10 @@ declare module "knex/types/tables" {
TProjectTemplatesUpdate
>;
[TableName.TotpConfig]: KnexOriginal.CompositeTableType<TTotpConfigs, TTotpConfigsInsert, TTotpConfigsUpdate>;
[TableName.ProjectSplitBackfillIds]: KnexOriginal.CompositeTableType<
TProjectSplitBackfillIds,
TProjectSplitBackfillIdsInsert,
TProjectSplitBackfillIdsUpdate
>;
}
}

@ -0,0 +1,59 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasAccessApprovalPolicyDeletedAtColumn = await knex.schema.hasColumn(
TableName.AccessApprovalPolicy,
"deletedAt"
);
const hasSecretApprovalPolicyDeletedAtColumn = await knex.schema.hasColumn(
TableName.SecretApprovalPolicy,
"deletedAt"
);
if (!hasAccessApprovalPolicyDeletedAtColumn) {
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
t.timestamp("deletedAt");
});
}
if (!hasSecretApprovalPolicyDeletedAtColumn) {
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
t.timestamp("deletedAt");
});
}
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
t.dropForeign(["privilegeId"]);
// Add the new foreign key constraint with ON DELETE SET NULL
t.foreign("privilegeId").references("id").inTable(TableName.ProjectUserAdditionalPrivilege).onDelete("SET NULL");
});
}
export async function down(knex: Knex): Promise<void> {
const hasAccessApprovalPolicyDeletedAtColumn = await knex.schema.hasColumn(
TableName.AccessApprovalPolicy,
"deletedAt"
);
const hasSecretApprovalPolicyDeletedAtColumn = await knex.schema.hasColumn(
TableName.SecretApprovalPolicy,
"deletedAt"
);
if (hasAccessApprovalPolicyDeletedAtColumn) {
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
t.dropColumn("deletedAt");
});
}
if (hasSecretApprovalPolicyDeletedAtColumn) {
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
t.dropColumn("deletedAt");
});
}
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
t.dropForeign(["privilegeId"]);
t.foreign("privilegeId").references("id").inTable(TableName.ProjectUserAdditionalPrivilege).onDelete("CASCADE");
});
}

@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.SecretVersionV2, "folderId")) {
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
t.index("folderId");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.SecretVersionV2, "folderId")) {
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
t.dropIndex("folderId");
});
}
}

@ -0,0 +1,297 @@
import slugify from "@sindresorhus/slugify";
import { Knex } from "knex";
import { v4 as uuidV4 } from "uuid";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ProjectType, TableName } from "../schemas";
/* eslint-disable no-await-in-loop,@typescript-eslint/ban-ts-comment */
const newProject = async (knex: Knex, projectId: string, projectType: ProjectType) => {
const newProjectId = uuidV4();
const project = await knex(TableName.Project).where("id", projectId).first();
await knex(TableName.Project).insert({
...project,
type: projectType,
// @ts-ignore id is required
id: newProjectId,
slug: slugify(`${project?.name}-${alphaNumericNanoId(4)}`)
});
const customRoleMapping: Record<string, string> = {};
const projectCustomRoles = await knex(TableName.ProjectRoles).where("projectId", projectId);
if (projectCustomRoles.length) {
await knex.batchInsert(
TableName.ProjectRoles,
projectCustomRoles.map((el) => {
const id = uuidV4();
customRoleMapping[el.id] = id;
return {
...el,
id,
projectId: newProjectId,
permissions: el.permissions ? JSON.stringify(el.permissions) : el.permissions
};
})
);
}
const groupMembershipMapping: Record<string, string> = {};
const groupMemberships = await knex(TableName.GroupProjectMembership).where("projectId", projectId);
if (groupMemberships.length) {
await knex.batchInsert(
TableName.GroupProjectMembership,
groupMemberships.map((el) => {
const id = uuidV4();
groupMembershipMapping[el.id] = id;
return { ...el, id, projectId: newProjectId };
})
);
}
const groupMembershipRoles = await knex(TableName.GroupProjectMembershipRole).whereIn(
"projectMembershipId",
groupMemberships.map((el) => el.id)
);
if (groupMembershipRoles.length) {
await knex.batchInsert(
TableName.GroupProjectMembershipRole,
groupMembershipRoles.map((el) => {
const id = uuidV4();
const projectMembershipId = groupMembershipMapping[el.projectMembershipId];
const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
return { ...el, id, projectMembershipId, customRoleId };
})
);
}
const identityProjectMembershipMapping: Record<string, string> = {};
const identities = await knex(TableName.IdentityProjectMembership).where("projectId", projectId);
if (identities.length) {
await knex.batchInsert(
TableName.IdentityProjectMembership,
identities.map((el) => {
const id = uuidV4();
identityProjectMembershipMapping[el.id] = id;
return { ...el, id, projectId: newProjectId };
})
);
}
const identitiesRoles = await knex(TableName.IdentityProjectMembershipRole).whereIn(
"projectMembershipId",
identities.map((el) => el.id)
);
if (identitiesRoles.length) {
await knex.batchInsert(
TableName.IdentityProjectMembershipRole,
identitiesRoles.map((el) => {
const id = uuidV4();
const projectMembershipId = identityProjectMembershipMapping[el.projectMembershipId];
const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
return { ...el, id, projectMembershipId, customRoleId };
})
);
}
const projectMembershipMapping: Record<string, string> = {};
const projectUserMembers = await knex(TableName.ProjectMembership).where("projectId", projectId);
if (projectUserMembers.length) {
await knex.batchInsert(
TableName.ProjectMembership,
projectUserMembers.map((el) => {
const id = uuidV4();
projectMembershipMapping[el.id] = id;
return { ...el, id, projectId: newProjectId };
})
);
}
const membershipRoles = await knex(TableName.ProjectUserMembershipRole).whereIn(
"projectMembershipId",
projectUserMembers.map((el) => el.id)
);
if (membershipRoles.length) {
await knex.batchInsert(
TableName.ProjectUserMembershipRole,
membershipRoles.map((el) => {
const id = uuidV4();
const projectMembershipId = projectMembershipMapping[el.projectMembershipId];
const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
return { ...el, id, projectMembershipId, customRoleId };
})
);
}
const kmsKeys = await knex(TableName.KmsKey).where("projectId", projectId).andWhere("isReserved", true);
if (kmsKeys.length) {
await knex.batchInsert(
TableName.KmsKey,
kmsKeys.map((el) => {
const id = uuidV4();
const slug = slugify(alphaNumericNanoId(8).toLowerCase());
return { ...el, id, slug, projectId: newProjectId };
})
);
}
const projectBot = await knex(TableName.ProjectBot).where("projectId", projectId).first();
if (projectBot) {
const newProjectBot = { ...projectBot, id: uuidV4(), projectId: newProjectId };
await knex(TableName.ProjectBot).insert(newProjectBot);
}
const projectKeys = await knex(TableName.ProjectKeys).where("projectId", projectId);
if (projectKeys.length) {
await knex.batchInsert(
TableName.ProjectKeys,
projectKeys.map((el) => {
const id = uuidV4();
return { ...el, id, projectId: newProjectId };
})
);
}
return newProjectId;
};
const BATCH_SIZE = 500;
export async function up(knex: Knex): Promise<void> {
const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds);
if (!hasSplitMappingTable) {
await knex.schema.createTable(TableName.ProjectSplitBackfillIds, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("sourceProjectId", 36).notNullable();
t.foreign("sourceProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.string("destinationProjectType").notNullable();
t.string("destinationProjectId", 36).notNullable();
t.foreign("destinationProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
});
}
const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type");
if (!hasTypeColumn) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.string("type");
});
let projectsToBeTyped;
do {
// eslint-disable-next-line no-await-in-loop
projectsToBeTyped = await knex(TableName.Project).whereNull("type").limit(BATCH_SIZE).select("id");
if (projectsToBeTyped.length) {
// eslint-disable-next-line no-await-in-loop
await knex(TableName.Project)
.whereIn(
"id",
projectsToBeTyped.map((el) => el.id)
)
.update({ type: ProjectType.SecretManager });
}
} while (projectsToBeTyped.length > 0);
const projectsWithCertificates = await knex(TableName.CertificateAuthority)
.distinct("projectId")
.select("projectId");
/* eslint-disable no-await-in-loop,no-param-reassign */
for (const { projectId } of projectsWithCertificates) {
const newProjectId = await newProject(knex, projectId, ProjectType.CertificateManager);
await knex(TableName.CertificateAuthority).where("projectId", projectId).update({ projectId: newProjectId });
await knex(TableName.PkiAlert).where("projectId", projectId).update({ projectId: newProjectId });
await knex(TableName.PkiCollection).where("projectId", projectId).update({ projectId: newProjectId });
await knex(TableName.ProjectSplitBackfillIds).insert({
sourceProjectId: projectId,
destinationProjectType: ProjectType.CertificateManager,
destinationProjectId: newProjectId
});
}
const projectsWithCmek = await knex(TableName.KmsKey)
.where("isReserved", false)
.whereNotNull("projectId")
.distinct("projectId")
.select("projectId");
for (const { projectId } of projectsWithCmek) {
if (projectId) {
const newProjectId = await newProject(knex, projectId, ProjectType.KMS);
await knex(TableName.KmsKey)
.where({
isReserved: false,
projectId
})
.update({ projectId: newProjectId });
await knex(TableName.ProjectSplitBackfillIds).insert({
sourceProjectId: projectId,
destinationProjectType: ProjectType.KMS,
destinationProjectId: newProjectId
});
}
}
/* eslint-enable */
await knex.schema.alterTable(TableName.Project, (t) => {
t.string("type").notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type");
const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds);
if (hasTypeColumn && hasSplitMappingTable) {
const splitProjectMappings = await knex(TableName.ProjectSplitBackfillIds).where({});
const certMapping = splitProjectMappings.filter(
(el) => el.destinationProjectType === ProjectType.CertificateManager
);
/* eslint-disable no-await-in-loop */
for (const project of certMapping) {
await knex(TableName.CertificateAuthority)
.where("projectId", project.destinationProjectId)
.update({ projectId: project.sourceProjectId });
await knex(TableName.PkiAlert)
.where("projectId", project.destinationProjectId)
.update({ projectId: project.sourceProjectId });
await knex(TableName.PkiCollection)
.where("projectId", project.destinationProjectId)
.update({ projectId: project.sourceProjectId });
}
/* eslint-enable */
const kmsMapping = splitProjectMappings.filter((el) => el.destinationProjectType === ProjectType.KMS);
/* eslint-disable no-await-in-loop */
for (const project of kmsMapping) {
await knex(TableName.KmsKey)
.where({
isReserved: false,
projectId: project.destinationProjectId
})
.update({ projectId: project.sourceProjectId });
}
/* eslint-enable */
await knex(TableName.ProjectMembership)
.whereIn(
"projectId",
splitProjectMappings.map((el) => el.destinationProjectId)
)
.delete();
await knex(TableName.ProjectRoles)
.whereIn(
"projectId",
splitProjectMappings.map((el) => el.destinationProjectId)
)
.delete();
await knex(TableName.Project)
.whereIn(
"id",
splitProjectMappings.map((el) => el.destinationProjectId)
)
.delete();
await knex.schema.alterTable(TableName.Project, (t) => {
t.dropColumn("type");
});
}
if (hasSplitMappingTable) {
await knex.schema.dropTableIfExists(TableName.ProjectSplitBackfillIds);
}
}

@ -15,7 +15,8 @@ export const AccessApprovalPoliciesSchema = z.object({
envId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
enforcementLevel: z.string().default("hard")
enforcementLevel: z.string().default("hard"),
deletedAt: z.date().nullable().optional()
});
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;

@ -65,6 +65,7 @@ export * from "./project-keys";
export * from "./project-memberships";
export * from "./project-roles";
export * from "./project-slack-configs";
export * from "./project-split-backfill-ids";
export * from "./project-templates";
export * from "./project-user-additional-privilege";
export * from "./project-user-membership-roles";

@ -106,6 +106,7 @@ export enum TableName {
SecretApprovalRequestSecretV2 = "secret_approval_requests_secrets_v2",
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
ProjectSplitBackfillIds = "project_split_backfill_ids",
// junction tables with tags
SecretV2JnTag = "secret_v2_tag_junction",
JnSecretTag = "secret_tag_junction",
@ -200,3 +201,9 @@ export enum IdentityAuthMethod {
OIDC_AUTH = "oidc-auth",
JWT_AUTH = "jwt-auth"
}
export enum ProjectType {
SecretManager = "secret-manager",
CertificateManager = "cert-manager",
KMS = "kms"
}

@ -0,0 +1,21 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const ProjectSplitBackfillIdsSchema = z.object({
id: z.string().uuid(),
sourceProjectId: z.string(),
destinationProjectType: z.string(),
destinationProjectId: z.string()
});
export type TProjectSplitBackfillIds = z.infer<typeof ProjectSplitBackfillIdsSchema>;
export type TProjectSplitBackfillIdsInsert = Omit<z.input<typeof ProjectSplitBackfillIdsSchema>, TImmutableDBKeys>;
export type TProjectSplitBackfillIdsUpdate = Partial<
Omit<z.input<typeof ProjectSplitBackfillIdsSchema>, TImmutableDBKeys>
>;

@ -24,7 +24,8 @@ export const ProjectsSchema = z.object({
auditLogsRetentionDays: z.number().nullable().optional(),
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
description: z.string().nullable().optional()
description: z.string().nullable().optional(),
type: z.string()
});
export type TProjects = z.infer<typeof ProjectsSchema>;

@ -15,7 +15,8 @@ export const SecretApprovalPoliciesSchema = z.object({
envId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
enforcementLevel: z.string().default("hard")
enforcementLevel: z.string().default("hard"),
deletedAt: z.date().nullable().optional()
});
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;

@ -4,7 +4,7 @@ import { Knex } from "knex";
import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { ProjectMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [
@ -24,6 +24,7 @@ export async function seed(knex: Knex): Promise<void> {
name: seedData1.project.name,
orgId: seedData1.organization.id,
slug: "first-project",
type: ProjectType.SecretManager,
// eslint-disable-next-line
// @ts-ignore
id: seedData1.project.id

@ -1,6 +1,6 @@
import { Knex } from "knex";
import { ProjectMembershipRole, ProjectVersion, TableName } from "../schemas";
import { ProjectMembershipRole, ProjectType, ProjectVersion, TableName } from "../schemas";
import { seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [
@ -16,6 +16,7 @@ export async function seed(knex: Knex): Promise<void> {
orgId: seedData1.organization.id,
slug: seedData1.projectV3.slug,
version: ProjectVersion.V3,
type: ProjectType.SecretManager,
// eslint-disable-next-line
// @ts-ignore
id: seedData1.projectV3.id

@ -109,7 +109,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
approvers: z.string().array(),
secretPath: z.string().nullish(),
envId: z.string(),
enforcementLevel: z.string()
enforcementLevel: z.string(),
deletedAt: z.date().nullish()
}),
reviewers: z
.object({

@ -1,6 +1,7 @@
import { z } from "zod";
import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas";
import { EFilterReturnedUsers } from "@app/ee/services/group/group-types";
import { GROUPS } from "@app/lib/api-docs";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -151,7 +152,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit),
username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username),
search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search)
search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search),
filter: z.nativeEnum(EFilterReturnedUsers).optional().describe(GROUPS.LIST_USERS.filterUsers)
}),
response: {
200: z.object({
@ -164,7 +166,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
})
.merge(
z.object({
isPartOfGroup: z.boolean()
isPartOfGroup: z.boolean(),
joinedGroupAt: z.date().nullable()
})
)
.array(),

@ -52,7 +52,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
})
.array(),
secretPath: z.string().optional().nullable(),
enforcementLevel: z.string()
enforcementLevel: z.string(),
deletedAt: z.date().nullish()
}),
committerUser: approvalRequestUser,
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
@ -260,7 +261,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
approvals: z.number(),
approvers: approvalRequestUser.array(),
secretPath: z.string().optional().nullable(),
enforcementLevel: z.string()
enforcementLevel: z.string(),
deletedAt: z.date().nullish()
}),
environment: z.string(),
statusChangedByUser: approvalRequestUser.optional(),

@ -139,5 +139,10 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
}
};
return { ...accessApprovalPolicyOrm, find, findById };
const softDeleteById = async (policyId: string, tx?: Knex) => {
const softDeletedPolicy = await accessApprovalPolicyOrm.updateById(policyId, { deletedAt: new Date() }, tx);
return softDeletedPolicy;
};
return { ...accessApprovalPolicyOrm, find, findById, softDeleteById };
};

@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { ProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
@ -8,7 +9,11 @@ import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal";
import { TAccessApprovalRequestReviewerDALFactory } from "../access-approval-request/access-approval-request-reviewer-dal";
import { ApprovalStatus } from "../access-approval-request/access-approval-request-types";
import { TGroupDALFactory } from "../group/group-dal";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
import {
@ -21,7 +26,7 @@ import {
TUpdateAccessApprovalPolicy
} from "./access-approval-policy-types";
type TSecretApprovalPolicyServiceFactoryDep = {
type TAccessApprovalPolicyServiceFactoryDep = {
projectDAL: TProjectDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory;
@ -30,6 +35,9 @@ type TSecretApprovalPolicyServiceFactoryDep = {
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
groupDAL: TGroupDALFactory;
userDAL: Pick<TUserDALFactory, "find">;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update" | "find">;
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update">;
};
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
@ -41,8 +49,11 @@ export const accessApprovalPolicyServiceFactory = ({
permissionService,
projectEnvDAL,
projectDAL,
userDAL
}: TSecretApprovalPolicyServiceFactoryDep) => {
userDAL,
accessApprovalRequestDAL,
additionalPrivilegeDAL,
accessApprovalRequestReviewerDAL
}: TAccessApprovalPolicyServiceFactoryDep) => {
const createAccessApprovalPolicy = async ({
name,
actor,
@ -76,13 +87,15 @@ export const accessApprovalPolicyServiceFactory = ({
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval
@ -180,16 +193,9 @@ export const accessApprovalPolicyServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
// Anyone in the project should be able to get the policies.
/* const { permission } = */ await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
// ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId);
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id });
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null });
return accessApprovalPolicies;
};
@ -231,13 +237,14 @@ export const accessApprovalPolicyServiceFactory = ({
if (!accessApprovalPolicy) {
throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` });
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
accessApprovalPolicy.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
@ -314,19 +321,42 @@ export const accessApprovalPolicyServiceFactory = ({
const policy = await accessApprovalPolicyDAL.findById(policyId);
if (!policy) throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
policy.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretApproval
);
await accessApprovalPolicyDAL.deleteById(policyId);
await accessApprovalPolicyDAL.transaction(async (tx) => {
await accessApprovalPolicyDAL.softDeleteById(policyId, tx);
const allAccessApprovalRequests = await accessApprovalRequestDAL.find({ policyId });
if (allAccessApprovalRequests.length) {
const accessApprovalRequestsIds = allAccessApprovalRequests.map((request) => request.id);
const privilegeIdsArray = allAccessApprovalRequests
.map((request) => request.privilegeId)
.filter((id): id is string => id != null);
if (privilegeIdsArray.length) {
await additionalPrivilegeDAL.delete({ $in: { id: privilegeIdsArray } }, tx);
}
await accessApprovalRequestReviewerDAL.update(
{ $in: { id: accessApprovalRequestsIds }, status: ApprovalStatus.PENDING },
{ status: ApprovalStatus.REJECTED },
tx
);
}
});
return policy;
};
@ -356,7 +386,11 @@ export const accessApprovalPolicyServiceFactory = ({
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` });
const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id });
const policies = await accessApprovalPolicyDAL.find({
envId: environment.id,
projectId: project.id,
deletedAt: null
});
if (!policies) throw new NotFoundError({ message: `No policies found in environment with slug '${envSlug}'` });
return { count: policies.length };

@ -61,7 +61,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId")
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
)
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
@ -118,7 +119,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
approvals: doc.policyApprovals,
secretPath: doc.policySecretPath,
enforcementLevel: doc.policyEnforcementLevel,
envId: doc.policyEnvId
envId: doc.policyEnvId,
deletedAt: doc.policyDeletedAt
},
requestedByUser: {
userId: doc.requestedByUserId,
@ -141,7 +143,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
}
: null,
isApproved: !!doc.privilegeId
isApproved: !!doc.policyDeletedAt || !!doc.privilegeId
}),
childrenMapper: [
{
@ -252,7 +254,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals")
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
tx.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
);
const findById = async (id: string, tx?: Knex) => {
@ -271,7 +274,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
name: el.policyName,
approvals: el.policyApprovals,
secretPath: el.policySecretPath,
enforcementLevel: el.policyEnforcementLevel
enforcementLevel: el.policyEnforcementLevel,
deletedAt: el.policyDeletedAt
},
requestedByUser: {
userId: el.requestedByUserId,
@ -363,6 +367,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
)
.where(`${TableName.Environment}.projectId`, projectId)
.where(`${TableName.AccessApprovalPolicy}.deletedAt`, null)
.select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"));

@ -130,6 +130,9 @@ export const accessApprovalRequestServiceFactory = ({
message: `No policy in environment with slug '${environment.slug}' and with secret path '${secretPath}' was found.`
});
}
if (policy.deletedAt) {
throw new BadRequestError({ message: "The policy linked to this request has been deleted" });
}
const approverIds: string[] = [];
const approverGroupIds: string[] = [];
@ -309,6 +312,12 @@ export const accessApprovalRequestServiceFactory = ({
}
const { policy } = accessApprovalRequest;
if (policy.deletedAt) {
throw new BadRequestError({
message: "The policy associated with this access request has been deleted."
});
}
const { membership, hasRole } = await permissionService.getProjectPermission(
actor,
actorId,

@ -60,6 +60,7 @@ export enum EventType {
DELETE_SECRETS = "delete-secrets",
GET_WORKSPACE_KEY = "get-workspace-key",
AUTHORIZE_INTEGRATION = "authorize-integration",
UPDATE_INTEGRATION_AUTH = "update-integration-auth",
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
CREATE_INTEGRATION = "create-integration",
DELETE_INTEGRATION = "delete-integration",
@ -362,6 +363,13 @@ interface AuthorizeIntegrationEvent {
};
}
interface UpdateIntegrationAuthEvent {
type: EventType.UPDATE_INTEGRATION_AUTH;
metadata: {
integration: string;
};
}
interface UnauthorizeIntegrationEvent {
type: EventType.UNAUTHORIZE_INTEGRATION;
metadata: {
@ -1746,6 +1754,7 @@ export type Event =
| DeleteSecretBatchEvent
| GetWorkspaceKeyEvent
| AuthorizeIntegrationEvent
| UpdateIntegrationAuthEvent
| UnauthorizeIntegrationEvent
| CreateIntegrationEvent
| DeleteIntegrationEvent

@ -1,7 +1,7 @@
import { ForbiddenError, subject } from "@casl/ability";
import ms from "ms";
import { SecretKeyEncoding } from "@app/db/schemas";
import { ProjectType, SecretKeyEncoding } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
@ -67,13 +67,14 @@ export const dynamicSecretLeaseServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -146,13 +147,14 @@ export const dynamicSecretLeaseServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -225,13 +227,14 @@ export const dynamicSecretLeaseServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })

@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability";
import { SecretKeyEncoding } from "@app/db/schemas";
import { ProjectType, SecretKeyEncoding } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
@ -73,13 +73,14 @@ export const dynamicSecretServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.CreateRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -144,13 +145,14 @@ export const dynamicSecretServiceFactory = ({
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.EditRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -227,13 +229,14 @@ export const dynamicSecretServiceFactory = ({
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })

@ -5,6 +5,8 @@ import { TableName, TGroups } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
import { EFilterReturnedUsers } from "./group-types";
export type TGroupDALFactory = ReturnType<typeof groupDALFactory>;
export const groupDALFactory = (db: TDbClient) => {
@ -66,7 +68,8 @@ export const groupDALFactory = (db: TDbClient) => {
offset = 0,
limit,
username, // depreciated in favor of search
search
search,
filter
}: {
orgId: string;
groupId: string;
@ -74,6 +77,7 @@ export const groupDALFactory = (db: TDbClient) => {
limit?: number;
username?: string;
search?: string;
filter?: EFilterReturnedUsers;
}) => {
try {
const query = db
@ -90,6 +94,7 @@ export const groupDALFactory = (db: TDbClient) => {
.select(
db.ref("id").withSchema(TableName.OrgMembership),
db.ref("groupId").withSchema(TableName.UserGroupMembership),
db.ref("createdAt").withSchema(TableName.UserGroupMembership).as("joinedGroupAt"),
db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
@ -111,17 +116,37 @@ export const groupDALFactory = (db: TDbClient) => {
void query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
}
switch (filter) {
case EFilterReturnedUsers.EXISTING_MEMBERS:
void query.andWhere(`${TableName.UserGroupMembership}.createdAt`, "is not", null);
break;
case EFilterReturnedUsers.NON_MEMBERS:
void query.andWhere(`${TableName.UserGroupMembership}.createdAt`, "is", null);
break;
default:
break;
}
const members = await query;
return {
members: members.map(
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({
({
email,
username: memberUsername,
firstName,
lastName,
userId,
groupId: memberGroupId,
joinedGroupAt
}) => ({
id: userId,
email,
username: memberUsername,
firstName,
lastName,
isPartOfGroup: !!memberGroupId
isPartOfGroup: !!memberGroupId,
joinedGroupAt
})
),
// @ts-expect-error col select is raw and not strongly typed

@ -222,7 +222,8 @@ export const groupServiceFactory = ({
actorId,
actorAuthMethod,
actorOrgId,
search
search,
filter
}: TListGroupUsersDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
@ -251,7 +252,8 @@ export const groupServiceFactory = ({
offset,
limit,
username,
search
search,
filter
});
return { users: members, totalCount };
@ -283,8 +285,8 @@ export const groupServiceFactory = ({
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPriviledges)
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPrivileges)
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
const user = await userDAL.findOne({ username });
@ -338,8 +340,8 @@ export const groupServiceFactory = ({
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPriviledges)
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPrivileges)
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
const user = await userDAL.findOne({ username });

@ -39,6 +39,7 @@ export type TListGroupUsersDTO = {
limit: number;
username?: string;
search?: string;
filter?: EFilterReturnedUsers;
} & TGenericPermission;
export type TAddUserToGroupDTO = {
@ -101,3 +102,8 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = {
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex;
};
export enum EFilterReturnedUsers {
EXISTING_MEMBERS = "existingMembers",
NON_MEMBERS = "nonMembers"
}

@ -269,6 +269,7 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project),
db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("id").withSchema(TableName.Project).as("projectId")
);
@ -284,13 +285,15 @@ export const permissionDALFactory = (db: TDbClient) => {
membershipCreatedAt,
groupMembershipCreatedAt,
groupMembershipUpdatedAt,
membershipUpdatedAt
membershipUpdatedAt,
projectType
}) => ({
orgId,
orgAuthEnforced,
userId,
projectId,
username,
projectType,
id: membershipId || groupMembershipId,
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
@ -449,6 +452,7 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
@ -480,7 +484,14 @@ export const permissionDALFactory = (db: TDbClient) => {
const permission = sqlNestRelationships({
data: docs,
key: "membershipId",
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({
parentMapper: ({
membershipId,
membershipCreatedAt,
membershipUpdatedAt,
orgId,
identityName,
projectType
}) => ({
id: membershipId,
identityId,
username: identityName,
@ -488,6 +499,7 @@ export const permissionDALFactory = (db: TDbClient) => {
createdAt: membershipCreatedAt,
updatedAt: membershipUpdatedAt,
orgId,
projectType,
// just a prefilled value
orgAuthEnforced: false
}),

@ -6,6 +6,7 @@ import handlebars from "handlebars";
import {
OrgMembershipRole,
ProjectMembershipRole,
ProjectType,
ServiceTokenScopes,
TIdentityProjectMemberships,
TProjectMemberships
@ -255,6 +256,13 @@ export const permissionServiceFactory = ({
return {
permission,
membership: userProjectPermission,
ForbidOnInvalidProjectType: (productType: ProjectType) => {
if (productType !== userProjectPermission.projectType) {
throw new BadRequestError({
message: `The project is of type ${userProjectPermission.projectType}. Operations of type ${productType} are not allowed.`
});
}
},
hasRole: (role: string) =>
userProjectPermission.roles.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
@ -323,6 +331,13 @@ export const permissionServiceFactory = ({
return {
permission,
membership: identityProjectPermission,
ForbidOnInvalidProjectType: (productType: ProjectType) => {
if (productType !== identityProjectPermission.projectType) {
throw new BadRequestError({
message: `The project is of type ${identityProjectPermission.projectType}. Operations of type ${productType} are not allowed.`
});
}
},
hasRole: (role: string) =>
identityProjectPermission.roles.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
@ -361,7 +376,14 @@ export const permissionServiceFactory = ({
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
return {
permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions),
membership: undefined
membership: undefined,
ForbidOnInvalidProjectType: (productType: ProjectType) => {
if (productType !== serviceTokenProject.type) {
throw new BadRequestError({
message: `The project is of type ${serviceTokenProject.type}. Operations of type ${productType} are not allowed.`
});
}
}
};
};
@ -370,6 +392,7 @@ export const permissionServiceFactory = ({
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
membership: undefined;
hasRole: (arg: string) => boolean;
ForbidOnInvalidProjectType: (type: ProjectType) => void;
} // service token doesn't have both membership and roles
: {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
@ -379,6 +402,7 @@ export const permissionServiceFactory = ({
roles: Array<{ role: string }>;
};
hasRole: (role: string) => boolean;
ForbidOnInvalidProjectType: (type: ProjectType) => void;
};
const getProjectPermission = async <T extends ActorType>(

@ -177,5 +177,10 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
}
};
return { ...secretApprovalPolicyOrm, findById, find };
const softDeleteById = async (policyId: string, tx?: Knex) => {
const softDeletedPolicy = await secretApprovalPolicyOrm.updateById(policyId, { deletedAt: new Date() }, tx);
return softDeletedPolicy;
};
return { ...secretApprovalPolicyOrm, findById, find, softDeleteById };
};

@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import picomatch from "picomatch";
import { ProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@ -11,6 +12,8 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { TSecretApprovalRequestDALFactory } from "../secret-approval-request/secret-approval-request-dal";
import { RequestState } from "../secret-approval-request/secret-approval-request-types";
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
import {
@ -34,6 +37,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "find">;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "update">;
};
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
@ -44,7 +48,8 @@ export const secretApprovalPolicyServiceFactory = ({
secretApprovalPolicyApproverDAL,
projectEnvDAL,
userDAL,
licenseService
licenseService,
secretApprovalRequestDAL
}: TSecretApprovalPolicyServiceFactoryDep) => {
const createSecretApprovalPolicy = async ({
name,
@ -74,13 +79,14 @@ export const secretApprovalPolicyServiceFactory = ({
if (!groupApprovers.length && approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval
@ -187,13 +193,14 @@ export const secretApprovalPolicyServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
secretApprovalPolicy.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
const plan = await licenseService.getPlan(actorOrgId);
@ -281,13 +288,14 @@ export const secretApprovalPolicyServiceFactory = ({
if (!sapPolicy)
throw new NotFoundError({ message: `Secret approval policy with ID '${secretPolicyId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
sapPolicy.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretApproval
@ -301,8 +309,16 @@ export const secretApprovalPolicyServiceFactory = ({
});
}
await secretApprovalPolicyDAL.deleteById(secretPolicyId);
return sapPolicy;
const deletedPolicy = await secretApprovalPolicyDAL.transaction(async (tx) => {
await secretApprovalRequestDAL.update(
{ policyId: secretPolicyId, status: RequestState.Open },
{ status: RequestState.Closed },
tx
);
const updatedPolicy = await secretApprovalPolicyDAL.softDeleteById(secretPolicyId, tx);
return updatedPolicy;
});
return { ...deletedPolicy, projectId: sapPolicy.projectId, environment: sapPolicy.environment };
};
const getSecretApprovalPolicyByProjectId = async ({
@ -321,7 +337,7 @@ export const secretApprovalPolicyServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
const sapPolicies = await secretApprovalPolicyDAL.find({ projectId });
const sapPolicies = await secretApprovalPolicyDAL.find({ projectId, deletedAt: null });
return sapPolicies;
};
@ -334,7 +350,7 @@ export const secretApprovalPolicyServiceFactory = ({
});
}
const policies = await secretApprovalPolicyDAL.find({ envId: env.id });
const policies = await secretApprovalPolicyDAL.find({ envId: env.id, deletedAt: null });
if (!policies.length) return;
// this will filter policies either without scoped to secret path or the one that matches with secret path
const policiesFilteredByPath = policies.filter(

@ -111,7 +111,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"),
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals")
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
tx.ref("deletedAt").withSchema(TableName.SecretApprovalPolicy).as("policyDeletedAt")
);
const findById = async (id: string, tx?: Knex) => {
@ -147,7 +148,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
approvals: el.policyApprovals,
secretPath: el.policySecretPath,
enforcementLevel: el.policyEnforcementLevel,
envId: el.policyEnvId
envId: el.policyEnvId,
deletedAt: el.policyDeletedAt
}
}),
childrenMapper: [
@ -222,6 +224,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicyApprover}.policyId`
)
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id`
)
.where({ projectId })
.andWhere(
(bd) =>
@ -229,6 +236,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
)
.andWhere((bd) => void bd.where(`${TableName.SecretApprovalPolicy}.deletedAt`, null))
.select("status", `${TableName.SecretApprovalRequest}.id`)
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
.count("status")

@ -2,6 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import {
ProjectMembershipRole,
ProjectType,
SecretEncryptionAlgo,
SecretKeyEncoding,
SecretType,
@ -232,10 +233,10 @@ export const secretApprovalRequestServiceFactory = ({
type: KmsDataKey.SecretManager,
projectId
});
const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2(
const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2(
secretApprovalRequest.id
);
secrets = encrypedSecrets.map((el) => ({
secrets = encryptedSecrets.map((el) => ({
...el,
secretKey: el.key,
id: el.id,
@ -274,8 +275,8 @@ export const secretApprovalRequestServiceFactory = ({
}));
} else {
if (!botKey) throw new NotFoundError({ message: `Project bot key not found`, name: "BotKeyNotFound" }); // CLI depends on this error message. TODO(daniel): Make API check for name BotKeyNotFound instead of message
const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
secrets = encrypedSecrets.map((el) => ({
const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
secrets = encryptedSecrets.map((el) => ({
...el,
...decryptSecretWithBot(el, botKey),
secret: el.secret
@ -323,6 +324,12 @@ export const secretApprovalRequestServiceFactory = ({
}
const { policy } = secretApprovalRequest;
if (policy.deletedAt) {
throw new BadRequestError({
message: "The policy associated with this secret approval request has been deleted."
});
}
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
actorId,
@ -383,6 +390,12 @@ export const secretApprovalRequestServiceFactory = ({
}
const { policy } = secretApprovalRequest;
if (policy.deletedAt) {
throw new BadRequestError({
message: "The policy associated with this secret approval request has been deleted."
});
}
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
actorId,
@ -433,6 +446,12 @@ export const secretApprovalRequestServiceFactory = ({
}
const { policy, folderId, projectId } = secretApprovalRequest;
if (policy.deletedAt) {
throw new BadRequestError({
message: "The policy associated with this secret approval request has been deleted."
});
}
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
actorId,
@ -857,13 +876,14 @@ export const secretApprovalRequestServiceFactory = ({
}: TGenerateSecretApprovalRequestDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
@ -1137,14 +1157,14 @@ export const secretApprovalRequestServiceFactory = ({
if (actor === ActorType.SERVICE || actor === ActorType.Machine)
throw new BadRequestError({ message: "Cannot use service token or machine token over protected branches" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
throw new NotFoundError({

@ -1,7 +1,7 @@
import { ForbiddenError, subject } from "@casl/ability";
import Ajv from "ajv";
import { ProjectVersion, TableName } from "@app/db/schemas";
import { ProjectType, ProjectVersion, TableName } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
@ -53,13 +53,14 @@ export const secretRotationServiceFactory = ({
actorAuthMethod,
projectId
}: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
return {
@ -81,13 +82,14 @@ export const secretRotationServiceFactory = ({
secretPath,
environment
}: TCreateSecretRotationDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRotation
@ -234,13 +236,14 @@ export const secretRotationServiceFactory = ({
message: "Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation."
});
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
doc.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
await secretRotationQueue.removeFromQueue(doc.id, doc.interval);
await secretRotationQueue.addToQueue(doc.id, doc.interval);
@ -251,13 +254,14 @@ export const secretRotationServiceFactory = ({
const doc = await secretRotationDAL.findById(rotationId);
if (!doc) throw new NotFoundError({ message: `Rotation with ID '${rotationId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
doc.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretRotation

@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability";
import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
import { ProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { InternalServerError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
@ -322,13 +322,14 @@ export const secretSnapshotServiceFactory = ({
if (!snapshot) throw new NotFoundError({ message: `Snapshot with ID '${snapshotId}' not found` });
const shouldUseBridge = snapshot.projectVersion === 3;
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
snapshot.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRollback

@ -19,7 +19,9 @@ export const GROUPS = {
offset: "The offset to start from. If you enter 10, it will start from the 10th user.",
limit: "The number of users to return.",
username: "The username to search for.",
search: "The text string that user email or name will be filtered by."
search: "The text string that user email or name will be filtered by.",
filterUsers:
"Whether to filter the list of returned users. 'existingMembers' will only return existing users in the group, 'nonMembers' will only return users not in the group, undefined will return all users in the organization."
},
ADD_USER: {
id: "The ID of the group to add the user to.",
@ -426,7 +428,8 @@ export const ORGANIZATIONS = {
search: "The text string that identity membership names will be filtered by."
},
GET_PROJECTS: {
organizationId: "The ID of the organization to get projects from."
organizationId: "The ID of the organization to get projects from.",
type: "The type of project to filter by."
},
LIST_GROUPS: {
organizationId: "The ID of the organization to list groups for."
@ -1078,6 +1081,9 @@ export const INTEGRATION_AUTH = {
DELETE_BY_ID: {
integrationAuthId: "The ID of integration authentication object to delete."
},
UPDATE_BY_ID: {
integrationAuthId: "The ID of integration authentication object to update."
},
CREATE_ACCESS_TOKEN: {
workspaceId: "The ID of the project to create the integration auth for.",
integration: "The slug of integration for the auth object.",
@ -1134,11 +1140,13 @@ export const INTEGRATION = {
},
UPDATE: {
integrationId: "The ID of the integration object.",
region: "AWS region to sync secrets to.",
app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
appId:
"The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
isActive: "Whether the integration should be active or disabled.",
secretPath: "The path of the secrets to sync secrets from.",
path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault.",
owner: "External integration providers service entity owner. Used in Github.",
targetEnvironment:
"The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.",

@ -166,8 +166,7 @@ const envSchema = z
OTEL_COLLECTOR_BASIC_AUTH_PASSWORD: zpStr(z.string().optional()),
OTEL_EXPORT_TYPE: z.enum(["prometheus", "otlp"]).optional(),
PLAIN_API_KEY: zpStr(z.string().optional()),
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
PYLON_API_KEY: zpStr(z.string().optional()),
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),

@ -57,7 +57,11 @@ const run = async () => {
const smtp = smtpServiceFactory(formatSmtpConfig());
const queue = queueServiceFactory(appCfg.REDIS_URL, appCfg.DB_CONNECTION_URI);
const queue = queueServiceFactory(appCfg.REDIS_URL, {
dbConnectionUrl: appCfg.DB_CONNECTION_URI,
dbRootCert: appCfg.DB_ROOT_CERT
});
await queue.initialize();
const keyStore = keyStoreFactory(appCfg.REDIS_URL);

@ -187,7 +187,10 @@ export type TQueueJobTypes = {
};
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
export const queueServiceFactory = (redisUrl: string, dbConnectionUrl: string) => {
export const queueServiceFactory = (
redisUrl: string,
{ dbConnectionUrl, dbRootCert }: { dbConnectionUrl: string; dbRootCert?: string }
) => {
const connection = new Redis(redisUrl, { maxRetriesPerRequest: null });
const queueContainer = {} as Record<
QueueName,
@ -198,7 +201,13 @@ export const queueServiceFactory = (redisUrl: string, dbConnectionUrl: string) =
connectionString: dbConnectionUrl,
archiveCompletedAfterSeconds: 60,
archiveFailedAfterSeconds: 1000, // we want to keep failed jobs for a longer time so that it can be retried
deleteAfterSeconds: 30
deleteAfterSeconds: 30,
ssl: dbRootCert
? {
rejectUnauthorized: true,
ca: Buffer.from(dbRootCert, "base64").toString("ascii")
}
: false
});
const queueContainerPg = {} as Record<QueueJobs, boolean>;

@ -417,7 +417,8 @@ export const registerRoutes = async (
permissionService,
secretApprovalPolicyDAL,
licenseService,
userDAL
userDAL,
secretApprovalRequestDAL
});
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
@ -756,7 +757,8 @@ export const registerRoutes = async (
pkiAlertDAL,
pkiCollectionDAL,
permissionService,
smtpService
smtpService,
projectDAL
});
const pkiCollectionService = pkiCollectionServiceFactory({
@ -764,7 +766,8 @@ export const registerRoutes = async (
pkiCollectionItemDAL,
certificateAuthorityDAL,
certificateDAL,
permissionService
permissionService,
projectDAL
});
const projectTemplateService = projectTemplateServiceFactory({
@ -997,7 +1000,10 @@ export const registerRoutes = async (
projectEnvDAL,
projectMembershipDAL,
projectDAL,
userDAL
userDAL,
accessApprovalRequestDAL,
additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL,
accessApprovalRequestReviewerDAL
});
const accessApprovalRequestService = accessApprovalRequestServiceFactory({
@ -1250,7 +1256,8 @@ export const registerRoutes = async (
});
const userEngagementService = userEngagementServiceFactory({
userDAL
userDAL,
orgDAL
});
const slackService = slackServiceFactory({
@ -1268,7 +1275,8 @@ export const registerRoutes = async (
const cmekService = cmekServiceFactory({
kmsDAL,
kmsService,
permissionService
permissionService,
projectDAL
});
const externalMigrationQueue = externalMigrationQueueFactory({

@ -220,6 +220,7 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
id: true,
name: true,
description: true,
type: true,
slug: true,
autoCapitalization: true,
orgId: true,

@ -328,7 +328,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true })
project: SanitizedProjectSchema.pick({ name: true, id: true, type: true })
})
)
})

@ -6,6 +6,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { OctopusDeployScope } from "@app/services/integration-auth/integration-auth-types";
import { Integrations } from "@app/services/integration-auth/integration-list";
import { integrationAuthPubSchema } from "../sanitizedSchemas";
@ -82,6 +83,67 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
}
});
server.route({
method: "PATCH",
url: "/:integrationAuthId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update the integration authentication object required for syncing secrets.",
security: [
{
bearerAuth: []
}
],
params: z.object({
integrationAuthId: z.string().trim().describe(INTEGRATION_AUTH.UPDATE_BY_ID.integrationAuthId)
}),
body: z.object({
integration: z.nativeEnum(Integrations).optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.integration),
accessId: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessId),
accessToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessToken),
awsAssumeIamRoleArn: z
.string()
.url()
.trim()
.optional()
.describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.awsAssumeIamRoleArn),
url: z.string().url().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.url),
namespace: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.namespace),
refreshToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.refreshToken)
}),
response: {
200: z.object({
integrationAuth: integrationAuthPubSchema
})
}
},
handler: async (req) => {
const integrationAuth = await server.services.integrationAuth.updateIntegrationAuth({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
integrationAuthId: req.params.integrationAuthId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: integrationAuth.projectId,
event: {
type: EventType.UPDATE_INTEGRATION_AUTH,
metadata: {
integration: integrationAuth.integration
}
}
});
return { integrationAuth };
}
});
server.route({
method: "DELETE",
url: "/",
@ -1123,4 +1185,50 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
return { spaces };
}
});
server.route({
method: "GET",
url: "/:integrationAuthId/circleci/organizations",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
integrationAuthId: z.string().trim()
}),
response: {
200: z.object({
organizations: z
.object({
name: z.string(),
slug: z.string(),
projects: z
.object({
name: z.string(),
id: z.string()
})
.array(),
contexts: z
.object({
name: z.string(),
id: z.string()
})
.array()
})
.array()
})
}
},
handler: async (req) => {
const organizations = await server.services.integrationAuth.getCircleCIOrganizations({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId
});
return { organizations };
}
});
};

@ -141,7 +141,9 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
targetEnvironment: z.string().trim().optional().describe(INTEGRATION.UPDATE.targetEnvironment),
owner: z.string().trim().optional().describe(INTEGRATION.UPDATE.owner),
environment: z.string().trim().optional().describe(INTEGRATION.UPDATE.environment),
metadata: IntegrationMetadataSchema.optional()
path: z.string().trim().optional().describe(INTEGRATION.UPDATE.path),
metadata: IntegrationMetadataSchema.optional(),
region: z.string().trim().optional().describe(INTEGRATION.UPDATE.region)
}),
response: {
200: z.object({

@ -5,6 +5,7 @@ import {
ProjectMembershipsSchema,
ProjectRolesSchema,
ProjectSlackConfigsSchema,
ProjectType,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
@ -135,7 +136,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
includeRoles: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.transform((value) => value === "true"),
type: z.enum([ProjectType.SecretManager, ProjectType.KMS, ProjectType.CertificateManager, "all"]).optional()
}),
response: {
200: z.object({
@ -154,7 +156,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
actorOrgId: req.permission.orgId,
type: req.query.type
});
return { workspaces };
}

@ -21,7 +21,7 @@ export const registerUserEngagementRouter = async (server: FastifyZodProvider) =
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
return server.services.userEngagement.createUserWish(req.permission.id, req.body.text);
return server.services.userEngagement.createUserWish(req.permission.id, req.permission.orgId, req.body.text);
}
});
};

@ -5,6 +5,7 @@ import {
OrgMembershipsSchema,
ProjectMembershipsSchema,
ProjectsSchema,
ProjectType,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
@ -78,6 +79,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
params: z.object({
organizationId: z.string().trim().describe(ORGANIZATIONS.GET_PROJECTS.organizationId)
}),
querystring: z.object({
type: z.nativeEnum(ProjectType).optional().describe(ORGANIZATIONS.GET_PROJECTS.type)
}),
response: {
200: z.object({
workspaces: z
@ -104,7 +108,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId
orgId: req.params.organizationId,
type: req.query.type
});
return { workspaces };
@ -281,7 +286,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
project: ProjectsSchema.pick({ name: true, id: true }),
project: ProjectsSchema.pick({ name: true, id: true, type: true }),
roles: z.array(
z.object({
id: z.string(),

@ -5,7 +5,8 @@ import {
CertificatesSchema,
PkiAlertsSchema,
PkiCollectionsSchema,
ProjectKeysSchema
ProjectKeysSchema,
ProjectType
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
@ -159,7 +160,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
template: slugSchema({ field: "Template Name", max: 64 })
.optional()
.default(InfisicalProjectTemplate.Default)
.describe(PROJECTS.CREATE.template)
.describe(PROJECTS.CREATE.template),
type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager)
}),
response: {
200: z.object({
@ -178,7 +180,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
workspaceDescription: req.body.projectDescription,
slug: req.body.slug,
kmsKeyId: req.body.kmsKeyId,
template: req.body.template
template: req.body.template,
type: req.body.type
});
await server.services.telemetry.sendPostHogEvents({

@ -5,7 +5,7 @@ import crypto, { KeyObject } from "crypto";
import ms from "ms";
import { z } from "zod";
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
@ -77,7 +77,10 @@ type TCertificateAuthorityServiceFactoryDep = {
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
projectDAL: Pick<
TProjectDALFactory,
"findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction" | "getProjectFromSplitId"
>;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
@ -123,14 +126,24 @@ export const certificateAuthorityServiceFactory = ({
}: TCreateCaDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
let projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
projectId,
ProjectType.CertificateManager
);
if (certManagerProjectFromSplit) {
projectId = certManagerProjectFromSplit.id;
}
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -161,7 +174,7 @@ export const certificateAuthorityServiceFactory = ({
const ca = await certificateAuthorityDAL.create(
{
projectId: project.id,
projectId,
type,
organization,
ou,
@ -185,7 +198,7 @@ export const certificateAuthorityServiceFactory = ({
);
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: project.id,
projectId,
projectDAL,
kmsService
});
@ -323,13 +336,14 @@ export const certificateAuthorityServiceFactory = ({
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
@ -348,13 +362,14 @@ export const certificateAuthorityServiceFactory = ({
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
@ -434,13 +449,14 @@ export const certificateAuthorityServiceFactory = ({
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -819,13 +835,14 @@ export const certificateAuthorityServiceFactory = ({
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: "CA not found" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -965,13 +982,14 @@ export const certificateAuthorityServiceFactory = ({
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -1127,13 +1145,14 @@ export const certificateAuthorityServiceFactory = ({
throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
@ -1455,13 +1474,14 @@ export const certificateAuthorityServiceFactory = ({
}
if (!dto.isInternal) {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
dto.actor,
dto.actorId,
ca.projectId,
dto.actorAuthMethod,
dto.actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,

@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import bcrypt from "bcrypt";
import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
import { ProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@ -67,13 +67,14 @@ export const certificateTemplateServiceFactory = ({
message: `CA with ID ${caId} not found`
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -128,13 +129,14 @@ export const certificateTemplateServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
@ -185,13 +187,14 @@ export const certificateTemplateServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
@ -252,13 +255,14 @@ export const certificateTemplateServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
@ -336,13 +340,14 @@ export const certificateTemplateServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,

@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { ProjectType } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@ -49,13 +50,14 @@ export const certificateServiceFactory = ({
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
@ -72,13 +74,14 @@ export const certificateServiceFactory = ({
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
@ -106,13 +109,14 @@ export const certificateServiceFactory = ({
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);

@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { ProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@ -14,24 +15,33 @@ import {
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
type TCmekServiceFactoryDep = {
kmsService: TKmsServiceFactory;
kmsDAL: TKmsKeyDALFactory;
permissionService: TPermissionServiceFactory;
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId">;
};
export type TCmekServiceFactory = ReturnType<typeof cmekServiceFactory>;
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => {
const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => {
const { permission } = await permissionService.getProjectPermission(
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, projectDAL }: TCmekServiceFactoryDep) => {
const createCmek = async ({ projectId: preSplitProjectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => {
let projectId = preSplitProjectId;
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS);
if (cmekProjectFromSplit) {
projectId = cmekProjectFromSplit.id;
}
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek);
const cmek = await kmsService.generateKmsKey({
@ -50,13 +60,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek);
@ -72,13 +83,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek);
@ -87,7 +99,16 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cmek;
};
const listCmeksByProjectId = async ({ projectId, ...filters }: TListCmeksByProjectIdDTO, actor: OrgServiceActor) => {
const listCmeksByProjectId = async (
{ projectId: preSplitProjectId, ...filters }: TListCmeksByProjectIdDTO,
actor: OrgServiceActor
) => {
let projectId = preSplitProjectId;
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(preSplitProjectId, ProjectType.KMS);
if (cmekProjectFromSplit) {
projectId = cmekProjectFromSplit.id;
}
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
@ -112,7 +133,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
@ -120,6 +141,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Encrypt, ProjectPermissionSub.Cmek);
const encrypt = await kmsService.encryptWithKmsKey({ kmsId: keyId });
@ -138,13 +160,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Decrypt, ProjectPermissionSub.Cmek);

@ -102,6 +102,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("projectId").withSchema(TableName.IdentityProjectMembership),
db.ref("name").as("projectName").withSchema(TableName.Project),
db.ref("type").as("projectType").withSchema(TableName.Project),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
@ -126,7 +127,8 @@ export const identityProjectDALFactory = (db: TDbClient) => {
createdAt,
updatedAt,
projectId,
projectName
projectName,
projectType
}) => ({
id,
identityId,
@ -147,7 +149,8 @@ export const identityProjectDALFactory = (db: TDbClient) => {
},
project: {
id: projectId,
name: projectName
name: projectName,
type: projectType
}
}),
key: "id",

@ -0,0 +1,5 @@
export type TCircleCIContext = {
id: string;
name: string;
created_at: string;
};

@ -4,13 +4,21 @@ import { Octokit } from "@octokit/rest";
import { Client as OctopusClient, SpaceRepository as OctopusSpaceRepository } from "@octopusdeploy/api-client";
import AWS from "aws-sdk";
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
import {
ProjectType,
SecretEncryptionAlgo,
SecretKeyEncoding,
TIntegrationAuths,
TIntegrationAuthsInsert
} from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { TGenericPermission, TProjectPermission } from "@app/lib/types";
import { TIntegrationDALFactory } from "../integration/integration-dal";
@ -18,6 +26,7 @@ import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { getApps } from "./integration-app-list";
import { TCircleCIContext } from "./integration-app-types";
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import {
@ -25,6 +34,7 @@ import {
TBitbucketEnvironment,
TBitbucketWorkspace,
TChecklyGroups,
TCircleCIOrganization,
TDeleteIntegrationAuthByIdDTO,
TDeleteIntegrationAuthsDTO,
TDuplicateGithubIntegrationAuthDTO,
@ -36,6 +46,7 @@ import {
TIntegrationAuthBitbucketEnvironmentsDTO,
TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthCircleCIOrganizationDTO,
TIntegrationAuthGithubEnvsDTO,
TIntegrationAuthGithubOrgsDTO,
TIntegrationAuthHerokuPipelinesDTO,
@ -55,6 +66,7 @@ import {
TOctopusDeployVariableSet,
TSaveIntegrationAccessTokenDTO,
TTeamCityBuildConfig,
TUpdateIntegrationAuthDTO,
TVercelBranches
} from "./integration-auth-types";
import { getIntegrationOptions, Integrations, IntegrationUrls } from "./integration-list";
@ -144,13 +156,14 @@ export const integrationAuthServiceFactory = ({
if (!Object.values(Integrations).includes(integration as Integrations))
throw new BadRequestError({ message: "Invalid integration" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
const tokenExchange = await exchangeCode({ integration, code, url, installationId });
@ -253,13 +266,14 @@ export const integrationAuthServiceFactory = ({
if (!Object.values(Integrations).includes(integration as Integrations))
throw new BadRequestError({ message: "Invalid integration" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
const updateDoc: TIntegrationAuthsInsert = {
@ -368,6 +382,148 @@ export const integrationAuthServiceFactory = ({
return integrationAuthDAL.create(updateDoc);
};
const updateIntegrationAuth = async ({
integrationAuthId,
refreshToken,
actorId,
integration: newIntegration,
url,
actor,
actorOrgId,
actorAuthMethod,
accessId,
namespace,
accessToken,
awsAssumeIamRoleArn
}: TUpdateIntegrationAuthDTO) => {
const integrationAuth = await integrationAuthDAL.findById(integrationAuthId);
if (!integrationAuth) {
throw new NotFoundError({ message: `Integration auth with id ${integrationAuthId} not found.` });
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
const { projectId } = integrationAuth;
const integration = newIntegration || integrationAuth.integration;
const updateDoc: TIntegrationAuthsInsert = {
projectId,
integration,
namespace,
url,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8,
...(integration === Integrations.GCP_SECRET_MANAGER
? {
metadata: {
authMethod: "serviceAccount"
}
}
: {})
};
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
if (refreshToken) {
const tokenDetails = await exchangeRefresh(
integration,
refreshToken,
url,
updateDoc.metadata as Record<string, string>
);
const refreshEncToken = secretManagerEncryptor({
plainText: Buffer.from(tokenDetails.refreshToken)
}).cipherTextBlob;
updateDoc.encryptedRefresh = refreshEncToken;
const accessEncToken = secretManagerEncryptor({
plainText: Buffer.from(tokenDetails.accessToken)
}).cipherTextBlob;
updateDoc.encryptedAccess = accessEncToken;
updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt;
}
if (!refreshToken && (accessId || accessToken || awsAssumeIamRoleArn)) {
if (accessToken) {
const accessEncToken = secretManagerEncryptor({
plainText: Buffer.from(accessToken)
}).cipherTextBlob;
updateDoc.encryptedAccess = accessEncToken;
updateDoc.encryptedAwsAssumeIamRoleArn = null;
}
if (accessId) {
const accessEncToken = secretManagerEncryptor({
plainText: Buffer.from(accessId)
}).cipherTextBlob;
updateDoc.encryptedAccessId = accessEncToken;
updateDoc.encryptedAwsAssumeIamRoleArn = null;
}
if (awsAssumeIamRoleArn) {
const awsAssumeIamRoleArnEncrypted = secretManagerEncryptor({
plainText: Buffer.from(awsAssumeIamRoleArn)
}).cipherTextBlob;
updateDoc.encryptedAwsAssumeIamRoleArn = awsAssumeIamRoleArnEncrypted;
updateDoc.encryptedAccess = null;
updateDoc.encryptedAccessId = null;
}
}
} else {
if (!botKey) throw new NotFoundError({ message: `Project bot key for project with ID '${projectId}' not found` });
if (refreshToken) {
const tokenDetails = await exchangeRefresh(
integration,
refreshToken,
url,
updateDoc.metadata as Record<string, string>
);
const refreshEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.refreshToken, botKey);
updateDoc.refreshIV = refreshEncToken.iv;
updateDoc.refreshTag = refreshEncToken.tag;
updateDoc.refreshCiphertext = refreshEncToken.ciphertext;
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.accessToken, botKey);
updateDoc.accessIV = accessEncToken.iv;
updateDoc.accessTag = accessEncToken.tag;
updateDoc.accessCiphertext = accessEncToken.ciphertext;
updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt;
}
if (!refreshToken && (accessId || accessToken || awsAssumeIamRoleArn)) {
if (accessToken) {
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessToken, botKey);
updateDoc.accessIV = accessEncToken.iv;
updateDoc.accessTag = accessEncToken.tag;
updateDoc.accessCiphertext = accessEncToken.ciphertext;
}
if (accessId) {
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessId, botKey);
updateDoc.accessIdIV = accessEncToken.iv;
updateDoc.accessIdTag = accessEncToken.tag;
updateDoc.accessIdCiphertext = accessEncToken.ciphertext;
}
if (awsAssumeIamRoleArn) {
const awsAssumeIamRoleArnEnc = encryptSymmetric128BitHexKeyUTF8(awsAssumeIamRoleArn, botKey);
updateDoc.awsAssumeIamRoleArnCipherText = awsAssumeIamRoleArnEnc.ciphertext;
updateDoc.awsAssumeIamRoleArnIV = awsAssumeIamRoleArnEnc.iv;
updateDoc.awsAssumeIamRoleArnTag = awsAssumeIamRoleArnEnc.tag;
}
}
}
return integrationAuthDAL.updateById(integrationAuthId, updateDoc);
};
// helper function
const getIntegrationAccessToken = async (
integrationAuth: TIntegrationAuths,
@ -1427,6 +1583,120 @@ export const integrationAuthServiceFactory = ({
return [];
};
const getCircleCIOrganizations = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
id
}: TIntegrationAuthCircleCIOrganizationDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const { data: organizations }: { data: TCircleCIOrganization[] } = await request.get(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
{
headers: {
"Circle-Token": `${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
let projects: {
orgName: string;
projectName: string;
projectId?: string;
}[] = [];
try {
const projectRes = (
await request.get<{ reponame: string; username: string; vcs_url: string }[]>(
`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`,
{
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
}
}
)
).data;
projects = projectRes.map((a) => ({
orgName: a.username, // username maps to unique organization name in CircleCI
projectName: a.reponame, // reponame maps to project name within an organization in CircleCI
projectId: a.vcs_url.split("/").pop() // vcs_url maps to the project id in CircleCI
}));
} catch (error) {
logger.error(error);
}
const projectsByOrg = groupBy(
projects.map((p) => ({
orgName: p.orgName,
name: p.projectName,
id: p.projectId as string
})),
(p) => p.orgName
);
const getOrgContexts = async (orgSlug: string) => {
type NextPageToken = string | null | undefined;
try {
const contexts: TCircleCIContext[] = [];
let nextPageToken: NextPageToken;
while (nextPageToken !== null) {
// eslint-disable-next-line no-await-in-loop
const { data } = await request.get<{
items: TCircleCIContext[];
next_page_token: NextPageToken;
}>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/context`, {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
},
params: new URLSearchParams({
"owner-slug": orgSlug,
...(nextPageToken ? { "page-token": nextPageToken } : {})
})
});
contexts.push(...data.items);
nextPageToken = data.next_page_token;
}
return contexts?.map((context) => ({
name: context.name,
id: context.id
}));
} catch (error) {
logger.error(error);
}
};
return Promise.all(
organizations.map(async (org) => ({
name: org.name,
slug: org.slug,
projects: projectsByOrg[org.name] ?? [],
contexts: (await getOrgContexts(org.slug)) ?? []
}))
);
};
const deleteIntegrationAuths = async ({
projectId,
integration,
@ -1615,6 +1885,7 @@ export const integrationAuthServiceFactory = ({
getIntegrationAuth,
oauthExchange,
saveIntegrationToken,
updateIntegrationAuth,
deleteIntegrationAuthById,
deleteIntegrationAuths,
getIntegrationAuthTeams,
@ -1638,6 +1909,7 @@ export const integrationAuthServiceFactory = ({
getTeamcityBuildConfigs,
getBitbucketWorkspaces,
getBitbucketEnvironments,
getCircleCIOrganizations,
getIntegrationAccessToken,
duplicateIntegrationAuth,
getOctopusDeploySpaces,

@ -22,6 +22,11 @@ export type TSaveIntegrationAccessTokenDTO = {
awsAssumeIamRoleArn?: string;
} & TProjectPermission;
export type TUpdateIntegrationAuthDTO = Omit<TSaveIntegrationAccessTokenDTO, "projectId" | "integration"> & {
integrationAuthId: string;
integration?: string;
};
export type TDeleteIntegrationAuthsDTO = TProjectPermission & {
integration: string;
projectId: string;
@ -123,6 +128,10 @@ export type TGetIntegrationAuthTeamCityBuildConfigDTO = {
appId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthCircleCIOrganizationDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TVercelBranches = {
ref: string;
lastCommit: string;
@ -184,6 +193,14 @@ export type TTeamCityBuildConfig = {
webUrl: string;
};
export type TCircleCIOrganization = {
id: string;
vcsType: string;
name: string;
avatarUrl: string;
slug: string;
};
export type TIntegrationsWithEnvironment = TIntegrations & {
environment?:
| {
@ -210,6 +227,11 @@ export enum OctopusDeployScope {
// add tenant, variable set, etc.
}
export enum CircleCiScope {
Project = "project",
Context = "context"
}
export type TOctopusDeployVariableSet = {
Id: string;
OwnerId: string;

@ -76,7 +76,6 @@ export enum IntegrationUrls {
RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2",
FLYIO_API_URL = "https://api.fly.io/graphql",
CIRCLECI_API_URL = "https://circleci.com/api",
DATABRICKS_API_URL = "https:/xxxx.com/api",
TRAVISCI_API_URL = "https://api.travis-ci.com",
SUPABASE_API_URL = "https://api.supabase.com",
LARAVELFORGE_API_URL = "https://forge.laravel.com",
@ -218,9 +217,9 @@ export const getIntegrationOptions = async () => {
docsLink: ""
},
{
name: "Circle CI",
name: "CircleCI",
slug: "circleci",
image: "Circle CI.png",
image: "CircleCI.png",
isAvailable: true,
type: "pat",
clientId: "",

@ -39,7 +39,12 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
import { OctopusDeployScope, TIntegrationsWithEnvironment, TOctopusDeployVariableSet } from "./integration-auth-types";
import {
CircleCiScope,
OctopusDeployScope,
TIntegrationsWithEnvironment,
TOctopusDeployVariableSet
} from "./integration-auth-types";
import {
IntegrationInitialSyncBehavior,
IntegrationMappingBehavior,
@ -2245,102 +2250,174 @@ const syncSecretsCircleCI = async ({
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
const getProjectSlug = async () => {
const requestConfig = {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
}
};
try {
const projectDetails = (
await request.get<{ slug: string }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`,
requestConfig
if (integration.scope === CircleCiScope.Context) {
// sync secrets to CircleCI
await Promise.all(
Object.keys(secrets).map(async (key) =>
request.put(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${key}`,
{
value: secrets[key].value
},
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
}
)
).data;
)
);
return projectDetails.slug;
} catch (err) {
if (err instanceof AxiosError) {
if (err.response?.data?.message !== "Not Found") {
throw new Error("Failed to get project slug from CircleCI during first attempt.");
}
}
}
// get secrets from CircleCI
const getSecretsRes = async () => {
type EnvVars = {
variable: string;
created_at: string;
updated_at: string;
context_id: string;
};
// For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization
try {
const circleCiOrganization = (
await request.get<{ slug: string; name: string }[]>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
requestConfig
)
).data;
let nextPageToken: string | null | undefined;
const envVars: EnvVars[] = [];
// Case 1: This is a new integration where the organization name is stored under `integration.owner`
if (integration.owner) {
const org = circleCiOrganization.find((o) => o.name === integration.owner);
if (org) {
return `${org.slug}/${integration.app}`;
}
}
// Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one
return `${circleCiOrganization[0].slug}/${integration.app}`;
} catch (err) {
throw new Error("Failed to get project slug from CircleCI during second attempt.");
}
};
const projectSlug = await getProjectSlug();
// sync secrets to CircleCI
await Promise.all(
Object.keys(secrets).map(async (key) =>
request.post(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{
name: key,
value: secrets[key].value
},
{
while (nextPageToken !== null) {
const res = await request.get<{
items: EnvVars[];
next_page_token: string | null;
}>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable`, {
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
}
)
)
);
"Accept-Encoding": "application/json"
},
params: nextPageToken
? new URLSearchParams({
"page-token": nextPageToken
})
: undefined
});
// get secrets from CircleCI
const getSecretsRes = (
await request.get<{ items: { name: string }[] }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{
envVars.push(...res.data.items);
nextPageToken = res.data.next_page_token;
}
return envVars;
};
// delete secrets from CircleCI
await Promise.all(
(await getSecretsRes()).map(async (sec) => {
if (!(sec.variable in secrets)) {
return request.delete(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${sec.variable}`,
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
}
);
}
})
);
} else {
const getProjectSlug = async () => {
const requestConfig = {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
}
}
)
).data?.items;
};
// delete secrets from CircleCI
await Promise.all(
getSecretsRes.map(async (sec) => {
if (!(sec.name in secrets)) {
return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, {
try {
const projectDetails = (
await request.get<{ slug: string }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`,
requestConfig
)
).data;
return projectDetails.slug;
} catch (err) {
if (err instanceof AxiosError) {
if (err.response?.data?.message !== "Not Found") {
throw new Error("Failed to get project slug from CircleCI during first attempt.");
}
}
}
// For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization
try {
const circleCiOrganization = (
await request.get<{ slug: string; name: string }[]>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
requestConfig
)
).data;
// Case 1: This is a new integration where the organization name is stored under `integration.owner`
if (integration.owner) {
const org = circleCiOrganization.find((o) => o.name === integration.owner);
if (org) {
return `${org.slug}/${integration.app}`;
}
}
// Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one
return `${circleCiOrganization[0].slug}/${integration.app}`;
} catch (err) {
throw new Error("Failed to get project slug from CircleCI during second attempt.");
}
};
const projectSlug = await getProjectSlug();
// sync secrets to CircleCI
await Promise.all(
Object.keys(secrets).map(async (key) =>
request.post(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{
name: key,
value: secrets[key].value
},
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
}
)
)
);
// get secrets from CircleCI
const getSecretsRes = (
await request.get<{ items: { name: string }[] }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
"Accept-Encoding": "application/json"
}
});
}
})
);
}
)
).data?.items;
// delete secrets from CircleCI
await Promise.all(
getSecretsRes.map(async (sec) => {
if (!(sec.name in secrets)) {
return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, {
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
});
}
})
);
}
};
/**

@ -1,5 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability";
import { ProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { NotFoundError } from "@app/lib/errors";
@ -80,13 +81,14 @@ export const integrationServiceFactory = ({
if (!integrationAuth)
throw new NotFoundError({ message: `Integration auth with ID '${integrationAuthId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
ForbiddenError.from(permission).throwUnlessCan(
@ -151,18 +153,21 @@ export const integrationServiceFactory = ({
isActive,
environment,
secretPath,
metadata
region,
metadata,
path
}: TUpdateIntegrationDTO) => {
const integration = await integrationDAL.findById(id);
if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
integration.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
const newEnvironment = environment || integration.environment.slug;
@ -192,7 +197,9 @@ export const integrationServiceFactory = ({
appId,
targetEnvironment,
owner,
region,
secretPath,
path,
metadata: {
...(integration.metadata as object),
...metadata
@ -289,13 +296,14 @@ export const integrationServiceFactory = ({
const integration = await integrationDAL.findById(id);
if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
integration.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId);

@ -49,6 +49,8 @@ export type TUpdateIntegrationDTO = {
appId?: string;
isActive?: boolean;
secretPath?: string;
region?: string;
path?: string;
targetEnvironment?: string;
owner?: string;
environment?: string;

@ -15,7 +15,6 @@ import {
TProjectUserMembershipRolesInsert,
TUsers
} from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
@ -196,26 +195,18 @@ export const orgServiceFactory = ({
return org;
};
const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => {
const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id));
let workspaces: (TProjects & { organization: string } & {
environments: {
id: string;
slug: string;
name: string;
}[];
})[];
const findAllWorkspaces = async ({ actor, actorId, orgId, type }: TFindAllWorkspacesDTO) => {
if (actor === ActorType.USER) {
workspaces = await projectDAL.findAllProjects(actorId);
} else if (actor === ActorType.IDENTITY) {
workspaces = await projectDAL.findAllProjectsByIdentity(actorId);
} else {
throw new BadRequestError({ message: "Invalid actor type" });
const workspaces = await projectDAL.findAllProjects(actorId, orgId, type || "all");
return workspaces;
}
return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id));
if (actor === ActorType.IDENTITY) {
const workspaces = await projectDAL.findAllProjectsByIdentity(actorId, type);
return workspaces;
}
throw new BadRequestError({ message: "Invalid actor type" });
};
const addGhostUser = async (orgId: string, tx?: Knex) => {

@ -1,3 +1,4 @@
import { ProjectType } from "@app/db/schemas";
import { TOrgPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType, MfaMethod } from "../auth/auth-type";
@ -55,6 +56,7 @@ export type TFindAllWorkspacesDTO = {
actorOrgId: string | undefined;
actorAuthMethod: ActorAuthMethod;
orgId: string;
type?: ProjectType;
};
export type TUpdateOrgDTO = {

@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { ProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
@ -8,6 +9,7 @@ import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-colle
import { pkiItemTypeToNameMap } from "@app/services/pki-collection/pki-collection-types";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TProjectDALFactory } from "../project/project-dal";
import { TPkiAlertDALFactory } from "./pki-alert-dal";
import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types";
@ -19,6 +21,7 @@ type TPkiAlertServiceFactoryDep = {
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
smtpService: Pick<TSmtpService, "sendMail">;
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId">;
};
export type TPkiAlertServiceFactory = ReturnType<typeof pkiAlertServiceFactory>;
@ -27,7 +30,8 @@ export const pkiAlertServiceFactory = ({
pkiAlertDAL,
pkiCollectionDAL,
permissionService,
smtpService
smtpService,
projectDAL
}: TPkiAlertServiceFactoryDep) => {
const sendPkiItemExpiryNotices = async () => {
const allAlertItems = await pkiAlertDAL.getExpiringPkiCollectionItemsForAlerting();
@ -63,7 +67,7 @@ export const pkiAlertServiceFactory = ({
};
const createPkiAlert = async ({
projectId,
projectId: preSplitProjectId,
name,
pkiCollectionId,
alertBeforeDays,
@ -73,13 +77,23 @@ export const pkiAlertServiceFactory = ({
actor,
actorOrgId
}: TCreateAlertDTO) => {
const { permission } = await permissionService.getProjectPermission(
let projectId = preSplitProjectId;
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
projectId,
ProjectType.CertificateManager
);
if (certManagerProjectFromSplit) {
projectId = certManagerProjectFromSplit.id;
}
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts);
@ -128,13 +142,14 @@ export const pkiAlertServiceFactory = ({
let alert = await pkiAlertDAL.findById(alertId);
if (!alert) throw new NotFoundError({ message: `Alert with ID '${alertId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
alert.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts);
@ -160,13 +175,14 @@ export const pkiAlertServiceFactory = ({
let alert = await pkiAlertDAL.findById(alertId);
if (!alert) throw new NotFoundError({ message: `Alert with ID '${alertId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
alert.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts);
alert = await pkiAlertDAL.deleteById(alertId);

@ -1,12 +1,13 @@
import { ForbiddenError } from "@casl/ability";
import { TPkiCollectionItems } from "@app/db/schemas";
import { ProjectType, TPkiCollectionItems } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TPkiCollectionDALFactory } from "./pki-collection-dal";
import { transformPkiCollectionItem } from "./pki-collection-fns";
import { TPkiCollectionItemDALFactory } from "./pki-collection-item-dal";
@ -30,6 +31,7 @@ type TPkiCollectionServiceFactoryDep = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findOne">;
certificateDAL: Pick<TCertificateDALFactory, "find">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId">;
};
export type TPkiCollectionServiceFactory = ReturnType<typeof pkiCollectionServiceFactory>;
@ -39,24 +41,35 @@ export const pkiCollectionServiceFactory = ({
pkiCollectionItemDAL,
certificateAuthorityDAL,
certificateDAL,
permissionService
permissionService,
projectDAL
}: TPkiCollectionServiceFactoryDep) => {
const createPkiCollection = async ({
name,
description,
projectId,
projectId: preSplitProjectId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TCreatePkiCollectionDTO) => {
const { permission } = await permissionService.getProjectPermission(
let projectId = preSplitProjectId;
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
projectId,
ProjectType.CertificateManager
);
if (certManagerProjectFromSplit) {
projectId = certManagerProjectFromSplit.id;
}
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -106,13 +119,14 @@ export const pkiCollectionServiceFactory = ({
let pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections);
pkiCollection = await pkiCollectionDAL.updateById(collectionId, {
@ -133,13 +147,14 @@ export const pkiCollectionServiceFactory = ({
let pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
@ -205,13 +220,14 @@ export const pkiCollectionServiceFactory = ({
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -298,13 +314,14 @@ export const pkiCollectionServiceFactory = ({
if (!pkiCollectionItem) throw new NotFoundError({ message: `PKI collection item with ID '${itemId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,

@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { ProjectType } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@ -41,13 +42,14 @@ export const projectEnvServiceFactory = ({
name,
slug
}: TCreateEnvDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
const lock = await keyStore
@ -129,13 +131,14 @@ export const projectEnvServiceFactory = ({
id,
position
}: TUpdateEnvDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
const lock = await keyStore
@ -192,13 +195,14 @@ export const projectEnvServiceFactory = ({
};
const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
const lock = await keyStore

@ -217,20 +217,33 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
db.ref("name").as("projectName").withSchema(TableName.Project),
db.ref("id").as("projectId").withSchema(TableName.Project)
db.ref("id").as("projectId").withSchema(TableName.Project),
db.ref("type").as("projectType").withSchema(TableName.Project)
)
.where({ isGhost: false });
const members = sqlNestRelationships({
data: docs,
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, projectId, projectName }) => ({
parentMapper: ({
email,
firstName,
username,
lastName,
publicKey,
isGhost,
id,
projectId,
projectName,
projectType
}) => ({
id,
userId,
projectId,
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
project: {
id: projectId,
name: projectName
name: projectName,
type: projectType
}
}),
key: "id",

@ -1,7 +1,14 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProjectsUpdate } from "@app/db/schemas";
import {
ProjectsSchema,
ProjectType,
ProjectUpgradeStatus,
ProjectVersion,
TableName,
TProjectsUpdate
} from "@app/db/schemas";
import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
@ -12,12 +19,18 @@ export type TProjectDALFactory = ReturnType<typeof projectDALFactory>;
export const projectDALFactory = (db: TDbClient) => {
const projectOrm = ormify(db, TableName.Project);
const findAllProjects = async (userId: string) => {
const findAllProjects = async (userId: string, orgId: string, projectType: ProjectType | "all") => {
try {
const workspaces = await db
.replicaNode()(TableName.ProjectMembership)
.where({ userId })
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.where(`${TableName.Project}.orgId`, orgId)
.andWhere((qb) => {
if (projectType !== "all") {
void qb.where(`${TableName.Project}.type`, projectType);
}
})
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
.select(
selectAllTableCols(TableName.Project),
@ -31,14 +44,17 @@ export const projectDALFactory = (db: TDbClient) => {
{ column: `${TableName.Environment}.position`, order: "asc" }
]);
const groups: string[] = await db(TableName.UserGroupMembership)
.where({ userId })
.select(selectAllTableCols(TableName.UserGroupMembership))
.pluck("groupId");
const groups = db(TableName.UserGroupMembership).where({ userId }).select("groupId");
const groupWorkspaces = await db(TableName.GroupProjectMembership)
.whereIn("groupId", groups)
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
.where(`${TableName.Project}.orgId`, orgId)
.andWhere((qb) => {
if (projectType !== "all") {
void qb.where(`${TableName.Project}.type`, projectType);
}
})
.whereNotIn(
`${TableName.Project}.id`,
workspaces.map(({ id }) => id)
@ -108,12 +124,17 @@ export const projectDALFactory = (db: TDbClient) => {
}
};
const findAllProjectsByIdentity = async (identityId: string) => {
const findAllProjectsByIdentity = async (identityId: string, projectType?: ProjectType) => {
try {
const workspaces = await db
.replicaNode()(TableName.IdentityProjectMembership)
.where({ identityId })
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`)
.andWhere((qb) => {
if (projectType) {
void qb.where(`${TableName.Project}.type`, projectType);
}
})
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
.select(
selectAllTableCols(TableName.Project),
@ -315,6 +336,22 @@ export const projectDALFactory = (db: TDbClient) => {
};
};
const getProjectFromSplitId = async (projectId: string, projectType: ProjectType) => {
try {
const project = await db(TableName.ProjectSplitBackfillIds)
.where({
sourceProjectId: projectId,
destinationProjectType: projectType
})
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectSplitBackfillIds}.destinationProjectId`)
.select(selectAllTableCols(TableName.Project))
.first();
return project;
} catch (error) {
throw new DatabaseError({ error, name: `Failed to find split project with id ${projectId}` });
}
};
return {
...projectOrm,
findAllProjects,
@ -325,6 +362,7 @@ export const projectDALFactory = (db: TDbClient) => {
findProjectByFilter,
findProjectBySlug,
findProjectWithOrg,
checkProjectUpgradeStatus
checkProjectUpgradeStatus,
getProjectFromSplitId
};
};

@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import { ProjectMembershipRole, ProjectVersion, TProjectEnvironments } from "@app/db/schemas";
import { ProjectMembershipRole, ProjectType, ProjectVersion, TProjectEnvironments } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@ -153,10 +153,10 @@ export const projectServiceFactory = ({
kmsKeyId,
tx: trx,
createDefaultEnvs = true,
template = InfisicalProjectTemplate.Default
template = InfisicalProjectTemplate.Default,
type = ProjectType.SecretManager
}: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId });
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
actor,
actorId,
@ -206,6 +206,7 @@ export const projectServiceFactory = ({
const project = await projectDAL.create(
{
name: workspaceName,
type,
description: workspaceDescription,
orgId: organization.id,
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
@ -430,11 +431,22 @@ export const projectServiceFactory = ({
return deletedProject;
};
const getProjects = async ({ actorId, includeRoles, actorAuthMethod, actorOrgId }: TListProjectsDTO) => {
const workspaces = await projectDAL.findAllProjects(actorId);
const getProjects = async ({
actorId,
includeRoles,
actorAuthMethod,
actorOrgId,
type = ProjectType.SecretManager
}: TListProjectsDTO) => {
const workspaces = await projectDAL.findAllProjects(actorId, actorOrgId, type);
if (includeRoles) {
const { permission } = await permissionService.getUserOrgPermission(actorId, actorOrgId, actorAuthMethod);
const { permission } = await permissionService.getUserOrgPermission(
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
// `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls.
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
@ -681,11 +693,19 @@ export const projectServiceFactory = ({
actor
}: TListProjectCasDTO) => {
const project = await projectDAL.findProjectByFilter(filter);
let projectId = project.id;
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
projectId,
ProjectType.CertificateManager
);
if (certManagerProjectFromSplit) {
projectId = certManagerProjectFromSplit.id;
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
projectId,
actorAuthMethod,
actorOrgId
);
@ -697,7 +717,7 @@ export const projectServiceFactory = ({
const cas = await certificateAuthorityDAL.find(
{
projectId: project.id,
projectId,
...(status && { status }),
...(friendlyName && { friendlyName }),
...(commonName && { commonName })
@ -723,18 +743,27 @@ export const projectServiceFactory = ({
actor
}: TListProjectCertsDTO) => {
const project = await projectDAL.findProjectByFilter(filter);
let projectId = project.id;
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
projectId,
ProjectType.CertificateManager
);
if (certManagerProjectFromSplit) {
projectId = certManagerProjectFromSplit.id;
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
const cas = await certificateAuthorityDAL.find({ projectId: project.id });
const cas = await certificateAuthorityDAL.find({ projectId });
const certificates = await certificateDAL.find(
{
@ -748,7 +777,7 @@ export const projectServiceFactory = ({
);
const count = await certificateDAL.countCertificatesInProject({
projectId: project.id,
projectId,
friendlyName,
commonName
});
@ -763,19 +792,29 @@ export const projectServiceFactory = ({
* Return list of (PKI) alerts configured for project
*/
const listProjectAlerts = async ({
projectId,
projectId: preSplitProjectId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TListProjectAlertsDTO) => {
const { permission } = await permissionService.getProjectPermission(
let projectId = preSplitProjectId;
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
projectId,
ProjectType.CertificateManager
);
if (certManagerProjectFromSplit) {
projectId = certManagerProjectFromSplit.id;
}
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
@ -790,19 +829,28 @@ export const projectServiceFactory = ({
* Return list of PKI collections for project
*/
const listProjectPkiCollections = async ({
projectId,
projectId: preSplitProjectId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TListProjectAlertsDTO) => {
const { permission } = await permissionService.getProjectPermission(
let projectId = preSplitProjectId;
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
projectId,
ProjectType.CertificateManager
);
if (certManagerProjectFromSplit) {
projectId = certManagerProjectFromSplit.id;
}
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
@ -817,19 +865,29 @@ export const projectServiceFactory = ({
* Return list of certificate templates for project
*/
const listProjectCertificateTemplates = async ({
projectId,
projectId: preSplitProjectId,
actorId,
actorOrgId,
actorAuthMethod,
actor
}: TListProjectCertificateTemplatesDTO) => {
const { permission } = await permissionService.getProjectPermission(
let projectId = preSplitProjectId;
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
projectId,
ProjectType.CertificateManager
);
if (certManagerProjectFromSplit) {
projectId = certManagerProjectFromSplit.id;
}
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,

@ -1,6 +1,6 @@
import { Knex } from "knex";
import { TProjectKeys } from "@app/db/schemas";
import { ProjectType, TProjectKeys } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
@ -35,6 +35,7 @@ export type TCreateProjectDTO = {
createDefaultEnvs?: boolean;
template?: string;
tx?: Knex;
type?: ProjectType;
};
export type TDeleteProjectBySlugDTO = {
@ -84,6 +85,7 @@ export type TDeleteProjectDTO = {
export type TListProjectsDTO = {
includeRoles: boolean;
type?: ProjectType | "all";
} & Omit<TProjectPermission, "projectId">;
export type TUpgradeProjectDTO = {

@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import path from "path";
import { v4 as uuidv4, validate as uuidValidate } from "uuid";
import { TSecretFoldersInsert } from "@app/db/schemas";
import { ProjectType, TSecretFoldersInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
@ -52,13 +52,14 @@ export const secretFolderServiceFactory = ({
environment,
path: secretPath
}: TCreateFolderDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -150,13 +151,14 @@ export const secretFolderServiceFactory = ({
throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
folders.forEach(({ environment, path: secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan(
@ -259,13 +261,14 @@ export const secretFolderServiceFactory = ({
path: secretPath,
id
}: TUpdateFolderDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
@ -339,13 +342,14 @@ export const secretFolderServiceFactory = ({
path: secretPath,
idOrName
}: TDeleteFolderDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,

@ -2,7 +2,7 @@ import path from "node:path";
import { ForbiddenError, subject } from "@casl/ability";
import { TableName } from "@app/db/schemas";
import { ProjectType, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@ -73,13 +73,14 @@ export const secretImportServiceFactory = ({
isReplication,
path: secretPath
}: TCreateSecretImportDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
// check if user has permission to import into destination path
ForbiddenError.from(permission).throwUnlessCan(
@ -189,13 +190,15 @@ export const secretImportServiceFactory = ({
data,
id
}: TUpdateSecretImportDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
@ -283,13 +286,15 @@ export const secretImportServiceFactory = ({
actorAuthMethod,
id
}: TDeleteSecretImportDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })

@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { ProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@ -23,7 +24,7 @@ export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory
export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => {
const createTag = async ({ slug, actor, color, actorId, actorOrgId, actorAuthMethod, projectId }: TCreateTagDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
@ -31,6 +32,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
const existingTag = await secretTagDAL.findOne({ slug, projectId });
if (existingTag) throw new BadRequestError({ message: "Tag already exist" });
@ -54,7 +56,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exist" });
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
tag.projectId,
@ -62,6 +64,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
const updatedTag = await secretTagDAL.updateById(tag.id, { color, slug });
return updatedTag;
@ -71,7 +74,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
const tag = await secretTagDAL.findById(id);
if (!tag) throw new NotFoundError({ message: `Tag with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
tag.projectId,
@ -79,6 +82,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
const deletedTag = await secretTagDAL.deleteById(tag.id);
return deletedTag;

@ -1,7 +1,7 @@
import { ForbiddenError, PureAbility, subject } from "@casl/ability";
import { z } from "zod";
import { ProjectMembershipRole, SecretsV2Schema, SecretType, TableName } from "@app/db/schemas";
import { ProjectMembershipRole, ProjectType, SecretsV2Schema, SecretType, TableName } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@ -188,13 +188,14 @@ export const secretV2BridgeServiceFactory = ({
secretPath,
...inputSecret
}: TCreateSecretDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
@ -310,13 +311,14 @@ export const secretV2BridgeServiceFactory = ({
secretPath,
...inputSecret
}: TUpdateSecretDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
if (inputSecret.newSecretName === "") {
throw new BadRequestError({ message: "New secret name cannot be empty" });
@ -494,13 +496,14 @@ export const secretV2BridgeServiceFactory = ({
secretPath,
...inputSecret
}: TDeleteSecretDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
@ -1081,13 +1084,14 @@ export const secretV2BridgeServiceFactory = ({
projectId,
secrets: inputSecrets
}: TCreateManySecretDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
@ -1221,13 +1225,14 @@ export const secretV2BridgeServiceFactory = ({
secretPath,
secrets: inputSecrets
}: TUpdateManySecretDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
@ -1427,13 +1432,14 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod,
actorOrgId
}: TDeleteManySecretDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
@ -1569,13 +1575,14 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId,
actorAuthMethod
}: TBackFillSecretReferencesDTO) => {
const { hasRole } = await permissionService.getProjectPermission(
const { hasRole, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
if (!hasRole(ProjectMembershipRole.Admin))
throw new ForbiddenRequestError({ message: "Only admins are allowed to take this action" });
@ -1616,13 +1623,14 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod,
actorOrgId
}: TMoveSecretsDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
const sourceFolder = await folderDAL.findBySecretPath(projectId, sourceEnvironment, sourceSecretPath);
if (!sourceFolder) {

@ -20,7 +20,8 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
.join(TableName.SecretV2, `${TableName.SecretV2}.id`, `${TableName.SecretVersionV2}.secretId`)
.join<TSecretVersionsV2, TSecretVersionsV2 & { secretId: string; max: number }>(
(tx || db)(TableName.SecretVersionV2)
.groupBy("folderId", "secretId")
.where(`${TableName.SecretVersionV2}.folderId`, folderId)
.groupBy("secretId")
.max("version")
.select("secretId")
.as("latestVersion"),

@ -4,6 +4,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import {
ProjectMembershipRole,
ProjectType,
ProjectUpgradeStatus,
SecretEncryptionAlgo,
SecretKeyEncoding,
@ -186,13 +187,15 @@ export const secretServiceFactory = ({
projectId,
...inputSecret
}: TCreateSecretDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
@ -301,13 +304,15 @@ export const secretServiceFactory = ({
projectId,
...inputSecret
}: TUpdateSecretDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
@ -443,13 +448,15 @@ export const secretServiceFactory = ({
projectId,
...inputSecret
}: TDeleteSecretDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
@ -732,13 +739,14 @@ export const secretServiceFactory = ({
projectId,
secrets: inputSecrets
}: TCreateBulkSecretDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
@ -817,13 +825,15 @@ export const secretServiceFactory = ({
projectId,
secrets: inputSecrets
}: TUpdateBulkSecretDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
@ -923,13 +933,14 @@ export const secretServiceFactory = ({
actorAuthMethod,
actorOrgId
}: TDeleteBulkSecretDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })

@ -53,6 +53,13 @@ export const smtpServiceFactory = (cfg: TSmtpConfig) => {
const smtp = createTransport(cfg);
const isSmtpOn = Boolean(cfg.host);
handlebars.registerHelper("emailFooter", () => {
const { SITE_URL } = getConfig();
return new handlebars.SafeString(
`<p style="font-size: 12px;">Email sent via Infisical at <a href="${SITE_URL}">${SITE_URL}</a></p>`
);
});
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {
const appCfg = getConfig();
const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8");

@ -45,6 +45,8 @@
View the request and approve or deny it
<a href="{{approvalUrl}}">here</a>.
</p>
{{emailFooter}}
</body>
</html>

@ -11,8 +11,11 @@
<p>A secret approval request has been bypassed in the project "{{projectName}}".</p>
<p>
{{requesterFullName}} ({{requesterEmail}}) has merged
a secret to environment {{environment}} at secret path {{secretPath}}
{{requesterFullName}}
({{requesterEmail}}) has merged a secret to environment
{{environment}}
at secret path
{{secretPath}}
without obtaining the required approvals.
</p>
<p>
@ -24,5 +27,7 @@
To review this action, please visit the request panel
<a href="{{approvalUrl}}">here</a>.
</p>
{{emailFooter}}
</body>
</html>

@ -1,4 +1,3 @@
<!DOCTYPE html>
<html>
<head>
@ -14,6 +13,8 @@
<h2>{{code}}</h2>
<p>The MFA code will be valid for 2 minutes.</p>
<p>Not you? Contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} immediately.</p>
{{emailFooter}}
</body>
</html>

@ -10,6 +10,8 @@
<h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started confirming your email.</p>
<h1>{{code}}</h1>
{{emailFooter}}
</body>
</html>

@ -16,6 +16,7 @@
<p>Error: {{error}}</p>
{{emailFooter}}
</body>
</html>

@ -12,6 +12,8 @@
{{provider}}
to Infisical is in progress. The import process may take up to 30 minutes, and you will receive once the import
has finished or if it fails.</p>
{{emailFooter}}
</body>
</html>

@ -9,6 +9,8 @@
<body>
<h2>An import from {{provider}} to Infisical was successful</h2>
<p>An import from {{provider}} was successful. Your data is now available in Infisical.</p>
{{emailFooter}}
</body>
</html>

@ -1,21 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Incident alert: secrets potentially leaked</title>
</head>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Incident alert: secrets potentially leaked</title>
</head>
<body>
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo</h3>
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
<body>
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo</h3>
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
<p>If these are production secrets, please rotate them immediately.</p>
<p>If these are production secrets, please rotate them immediately.</p>
<p>Once you have taken action, be sure to update the status of the risk in your <a
href="{{siteUrl}}">Infisical
dashboard</a>.</p>
</body>
<p>Once you have taken action, be sure to update the status of the risk in your
<a href="{{siteUrl}}">Infisical dashboard</a>.</p>
{{emailFooter}}
</body>
</html>

@ -26,6 +26,8 @@
{{#if syncMessage}}
<p><b>Reason: </b>{{syncMessage}}</p>
{{/if}}
{{emailFooter}}
</body>
</html>

@ -1,4 +1,3 @@
<!DOCTYPE html>
<html>
<head>
@ -13,7 +12,11 @@
<p><strong>Timestamp</strong>: {{timestamp}}</p>
<p><strong>IP address</strong>: {{ip}}</p>
<p><strong>User agent</strong>: {{userAgent}}</p>
<p>If you believe that this login is suspicious, please contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} or reset your password immediately.</p>
<p>If you believe that this login is suspicious, please contact
{{#if isCloud}}Infisical{{else}}your administrator{{/if}}
or reset your password immediately.</p>
{{emailFooter}}
</body>
</html>

@ -12,5 +12,7 @@
<a href="{{callback_url}}?token={{token}}{{#if metadata}}&metadata={{metadata}}{{/if}}&to={{email}}&organization_id={{organizationId}}">Click to join</a>
<h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
{{emailFooter}}
</body>
</html>

@ -1,14 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Account Recovery</title>
</head>
<body>
</head>
<body>
<h2>Reset your password</h2>
<p>Someone requested a password reset.</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
<p>If you didn't initiate this request, please contact {{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}</p>
</body>
<p>If you didn't initiate this request, please contact
{{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}</p>
{{emailFooter}}
</body>
</html>

@ -27,5 +27,7 @@
<p>Please take necessary actions to renew these items before they expire.</p>
<p>For more details, please log in to your Infisical account and check your PKI management section.</p>
{{emailFooter}}
</body>
</html>

@ -1,16 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Organization Invitation</title>
</head>
<body>
</head>
<body>
<h2>Join your organization on Infisical</h2>
<p>You've been invited to join the Infisical organization — {{organizationName}}</p>
<a href="{{callback_url}}">Join now</a>
<h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
</body>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
and configs.</p>
{{emailFooter}}
</body>
</html>

@ -17,6 +17,8 @@
View the request and approve or deny it
<a href="{{approvalUrl}}">here</a>.
</p>
{{emailFooter}}
</body>
</html>

@ -1,25 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Incident alert: secret leaked</title>
</head>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Incident alert: secret leaked</title>
</head>
<body>
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
by {{pusher_name}} ({{pusher_email}}). If
these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as comment
in the given programming. This will prevent future notifications from being sent out for those secret(s).</p>
<body>
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
by
{{pusher_name}}
({{pusher_email}}). If these are test secrets, please add `infisical-scan:ignore` at the end of the line
containing the secret as comment in the given programming. This will prevent future notifications from being sent
out for those secret(s).</p>
<p>If these are production secrets, please rotate them immediately.</p>
<p>If these are production secrets, please rotate them immediately.</p>
<p>Once you have taken action, be sure to update the status of the risk in your <a
href="{{siteUrl}}">Infisical
dashboard</a>.</p>
</body>
<p>Once you have taken action, be sure to update the status of the risk in your
<a href="{{siteUrl}}">Infisical dashboard</a>.</p>
{{emailFooter}}
</body>
</html>

@ -13,6 +13,8 @@
{{#if reminderNote}}
<p>Here's the note included with the reminder: {{reminderNote}}</p>
{{/if}}
{{emailFooter}}
</body>
</html>

@ -1,17 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Code</title>
</head>
</head>
<body>
<body>
<h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
<h1>{{code}}</h1>
<p>Questions about setting up Infisical? {{#if isCloud}}Email us at support@infisical.com{{else}}Contact your administrator{{/if}}.</p>
</body>
<p>Questions about setting up Infisical?
{{#if isCloud}}Email us at support@infisical.com{{else}}Contact your administrator{{/if}}.</p>
{{emailFooter}}
</body>
</html>

@ -11,6 +11,8 @@
<p>Your account has been temporarily locked due to multiple failed login attempts. </h2>
<a href="{{callback_url}}?token={{token}}">To unlock your account, follow the link here</a>
<p>If these attempts were not made by you, reset your password immediately.</p>
{{emailFooter}}
</body>
</html>

@ -11,5 +11,7 @@
<h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
and configs.</p>
{{emailFooter}}
</body>
</html>

@ -1,87 +1,44 @@
import { PlainClient } from "@team-plain/typescript-sdk";
import axios from "axios";
import { getConfig } from "@app/lib/config/env";
import { InternalServerError } from "@app/lib/errors";
import { TOrgDALFactory } from "../org/org-dal";
import { TUserDALFactory } from "../user/user-dal";
type TUserEngagementServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "findById">;
orgDAL: Pick<TOrgDALFactory, "findById">;
};
export type TUserEngagementServiceFactory = ReturnType<typeof userEngagementServiceFactory>;
export const userEngagementServiceFactory = ({ userDAL }: TUserEngagementServiceFactoryDep) => {
const createUserWish = async (userId: string, text: string) => {
export const userEngagementServiceFactory = ({ userDAL, orgDAL }: TUserEngagementServiceFactoryDep) => {
const createUserWish = async (userId: string, orgId: string, text: string) => {
const user = await userDAL.findById(userId);
const org = await orgDAL.findById(orgId);
const appCfg = getConfig();
if (!appCfg.PLAIN_API_KEY) {
if (!appCfg.PYLON_API_KEY) {
throw new InternalServerError({
message: "Plain is not configured."
message: "Pylon is not configured."
});
}
const client = new PlainClient({
apiKey: appCfg.PLAIN_API_KEY
});
const customerUpsertRes = await client.upsertCustomer({
identifier: {
emailAddress: user.email
},
onCreate: {
fullName: `${user.firstName} ${user.lastName}`,
shortName: user.firstName,
email: {
email: user.email as string,
isVerified: user.isEmailVerified as boolean
},
externalId: user.id
},
onUpdate: {
fullName: {
value: `${user.firstName} ${user.lastName}`
},
shortName: {
value: user.firstName
},
email: {
email: user.email as string,
isVerified: user.isEmailVerified as boolean
},
externalId: {
value: user.id
}
const request = axios.create({
baseURL: "https://api.usepylon.com",
headers: {
Authorization: `Bearer ${appCfg.PYLON_API_KEY}`
}
});
if (customerUpsertRes.error) {
throw new InternalServerError({ message: customerUpsertRes.error.message });
}
const createThreadRes = await client.createThread({
title: "Wish",
customerIdentifier: {
externalId: customerUpsertRes.data.customer.externalId
},
components: [
{
componentText: {
text
}
}
],
labelTypeIds: appCfg.PLAIN_WISH_LABEL_IDS?.split(",")
await request.post("/issues", {
title: `New Wish From: ${user.firstName} ${user.lastName} (${org.name})`,
body_html: text,
requester_email: user.email,
requester_name: `${user.firstName} ${user.lastName} (${org.name})`,
tags: ["wish"]
});
if (createThreadRes.error) {
throw new InternalServerError({
message: createThreadRes.error.message
});
}
};
return {
createUserWish

Binary file not shown.

Before

(image error) Size: 162 KiB

After

(image error) Size: 494 KiB

Some files were not shown because too many files have changed in this diff Show More