mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 16:26:16 +00:00
Merge branch 'main' into feat/addGroupsToSshHosts
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
name: Release Gateway Helm Chart
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-helm:
|
||||||
|
name: Release Helm Chart
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Helm
|
||||||
|
uses: azure/setup-helm@v3
|
||||||
|
with:
|
||||||
|
version: v3.10.0
|
||||||
|
|
||||||
|
- name: Install python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
|
||||||
|
- name: Install Cloudsmith CLI
|
||||||
|
run: pip install --upgrade cloudsmith-cli
|
||||||
|
|
||||||
|
- name: Build and push helm package to CloudSmith
|
||||||
|
run: cd helm-charts && sh upload-gateway-cloudsmith.sh
|
||||||
|
env:
|
||||||
|
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||||
@@ -24,3 +24,7 @@ frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65
|
|||||||
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
||||||
docs/documentation/platform/kms/overview.mdx:generic-api-key:281
|
docs/documentation/platform/kms/overview.mdx:generic-api-key:281
|
||||||
docs/documentation/platform/kms/overview.mdx:generic-api-key:344
|
docs/documentation/platform/kms/overview.mdx:generic-api-key:344
|
||||||
|
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:85
|
||||||
|
docs/cli/commands/user.mdx:generic-api-key:51
|
||||||
|
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:76
|
||||||
|
docs/integrations/app-connections/hashicorp-vault.mdx:generic-api-key:188
|
||||||
|
|||||||
@@ -69,6 +69,15 @@ module.exports = {
|
|||||||
["^\\."]
|
["^\\."]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"import/extensions": [
|
||||||
|
"error",
|
||||||
|
"ignorePackages",
|
||||||
|
{
|
||||||
|
"": "never", // this is required to get the .tsx to work...
|
||||||
|
ts: "never",
|
||||||
|
tsx: "never"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+2959
-209
File diff suppressed because it is too large
Load Diff
@@ -72,7 +72,8 @@
|
|||||||
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||||
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
|
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
|
||||||
"seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
"seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||||
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
|
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest",
|
||||||
|
"email:dev": "email dev --dir src/services/smtp/emails"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -96,6 +97,7 @@
|
|||||||
"@types/picomatch": "^2.3.3",
|
"@types/picomatch": "^2.3.3",
|
||||||
"@types/pkcs11js": "^1.0.4",
|
"@types/pkcs11js": "^1.0.4",
|
||||||
"@types/prompt-sync": "^4.2.3",
|
"@types/prompt-sync": "^4.2.3",
|
||||||
|
"@types/react": "^19.1.2",
|
||||||
"@types/resolve": "^1.20.6",
|
"@types/resolve": "^1.20.6",
|
||||||
"@types/safe-regex": "^1.1.6",
|
"@types/safe-regex": "^1.1.6",
|
||||||
"@types/sjcl": "^1.0.34",
|
"@types/sjcl": "^1.0.34",
|
||||||
@@ -115,6 +117,7 @@
|
|||||||
"nodemon": "^3.0.2",
|
"nodemon": "^3.0.2",
|
||||||
"pino-pretty": "^10.2.3",
|
"pino-pretty": "^10.2.3",
|
||||||
"prompt-sync": "^4.2.0",
|
"prompt-sync": "^4.2.0",
|
||||||
|
"react-email": "4.0.7",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsc-alias": "^1.8.8",
|
"tsc-alias": "^1.8.8",
|
||||||
@@ -164,6 +167,7 @@
|
|||||||
"@opentelemetry/semantic-conventions": "^1.27.0",
|
"@opentelemetry/semantic-conventions": "^1.27.0",
|
||||||
"@peculiar/asn1-schema": "^2.3.8",
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
"@peculiar/x509": "^1.12.1",
|
"@peculiar/x509": "^1.12.1",
|
||||||
|
"@react-email/components": "0.0.36",
|
||||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||||
"@sindresorhus/slugify": "1.1.0",
|
"@sindresorhus/slugify": "1.1.0",
|
||||||
"@slack/oauth": "^3.0.2",
|
"@slack/oauth": "^3.0.2",
|
||||||
@@ -175,6 +179,7 @@
|
|||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"axios-retry": "^4.0.0",
|
"axios-retry": "^4.0.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"botbuilder": "^4.23.2",
|
||||||
"bullmq": "^5.4.2",
|
"bullmq": "^5.4.2",
|
||||||
"cassandra-driver": "^4.7.2",
|
"cassandra-driver": "^4.7.2",
|
||||||
"connect-redis": "^7.1.1",
|
"connect-redis": "^7.1.1",
|
||||||
@@ -222,6 +227,8 @@
|
|||||||
"posthog-node": "^3.6.2",
|
"posthog-node": "^3.6.2",
|
||||||
"probot": "^13.3.8",
|
"probot": "^13.3.8",
|
||||||
"re2": "^1.21.4",
|
"re2": "^1.21.4",
|
||||||
|
"react": "19.1.0",
|
||||||
|
"react-dom": "19.1.0",
|
||||||
"safe-regex": "^2.1.1",
|
"safe-regex": "^2.1.1",
|
||||||
"scim-patch": "^0.8.3",
|
"scim-patch": "^0.8.3",
|
||||||
"scim2-parse-filter": "^0.2.10",
|
"scim2-parse-filter": "^0.2.10",
|
||||||
|
|||||||
Vendored
+4
@@ -41,6 +41,7 @@ import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/
|
|||||||
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
||||||
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
||||||
import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service";
|
import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service";
|
||||||
|
import { TSshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service";
|
||||||
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
||||||
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
@@ -71,6 +72,7 @@ import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-a
|
|||||||
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
||||||
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
||||||
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||||
|
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||||
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||||
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
||||||
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||||
@@ -213,6 +215,7 @@ declare module "fastify" {
|
|||||||
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
|
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
|
||||||
sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
|
sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
|
||||||
sshHost: TSshHostServiceFactory;
|
sshHost: TSshHostServiceFactory;
|
||||||
|
sshHostGroup: TSshHostGroupServiceFactory;
|
||||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||||
certificateEst: TCertificateEstServiceFactory;
|
certificateEst: TCertificateEstServiceFactory;
|
||||||
@@ -246,6 +249,7 @@ declare module "fastify" {
|
|||||||
kmipOperation: TKmipOperationServiceFactory;
|
kmipOperation: TKmipOperationServiceFactory;
|
||||||
gateway: TGatewayServiceFactory;
|
gateway: TGatewayServiceFactory;
|
||||||
secretRotationV2: TSecretRotationV2ServiceFactory;
|
secretRotationV2: TSecretRotationV2ServiceFactory;
|
||||||
|
microsoftTeams: TMicrosoftTeamsServiceFactory;
|
||||||
assumePrivileges: TAssumePrivilegeServiceFactory;
|
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||||
githubOrgSync: TGithubOrgSyncServiceFactory;
|
githubOrgSync: TGithubOrgSyncServiceFactory;
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+36
@@ -386,6 +386,12 @@ import {
|
|||||||
TSshCertificateTemplates,
|
TSshCertificateTemplates,
|
||||||
TSshCertificateTemplatesInsert,
|
TSshCertificateTemplatesInsert,
|
||||||
TSshCertificateTemplatesUpdate,
|
TSshCertificateTemplatesUpdate,
|
||||||
|
TSshHostGroupMemberships,
|
||||||
|
TSshHostGroupMembershipsInsert,
|
||||||
|
TSshHostGroupMembershipsUpdate,
|
||||||
|
TSshHostGroups,
|
||||||
|
TSshHostGroupsInsert,
|
||||||
|
TSshHostGroupsUpdate,
|
||||||
TSshHostLoginUserMappings,
|
TSshHostLoginUserMappings,
|
||||||
TSshHostLoginUserMappingsInsert,
|
TSshHostLoginUserMappingsInsert,
|
||||||
TSshHostLoginUserMappingsUpdate,
|
TSshHostLoginUserMappingsUpdate,
|
||||||
@@ -426,6 +432,16 @@ import {
|
|||||||
TWorkflowIntegrationsInsert,
|
TWorkflowIntegrationsInsert,
|
||||||
TWorkflowIntegrationsUpdate
|
TWorkflowIntegrationsUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import {
|
||||||
|
TMicrosoftTeamsIntegrations,
|
||||||
|
TMicrosoftTeamsIntegrationsInsert,
|
||||||
|
TMicrosoftTeamsIntegrationsUpdate
|
||||||
|
} from "@app/db/schemas/microsoft-teams-integrations";
|
||||||
|
import {
|
||||||
|
TProjectMicrosoftTeamsConfigs,
|
||||||
|
TProjectMicrosoftTeamsConfigsInsert,
|
||||||
|
TProjectMicrosoftTeamsConfigsUpdate
|
||||||
|
} from "@app/db/schemas/project-microsoft-teams-configs";
|
||||||
import {
|
import {
|
||||||
TSecretReminderRecipients,
|
TSecretReminderRecipients,
|
||||||
TSecretReminderRecipientsInsert,
|
TSecretReminderRecipientsInsert,
|
||||||
@@ -445,6 +461,16 @@ declare module "knex/types/tables" {
|
|||||||
interface Tables {
|
interface Tables {
|
||||||
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
||||||
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
||||||
|
[TableName.SshHostGroup]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshHostGroups,
|
||||||
|
TSshHostGroupsInsert,
|
||||||
|
TSshHostGroupsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SshHostGroupMembership]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshHostGroupMemberships,
|
||||||
|
TSshHostGroupMembershipsInsert,
|
||||||
|
TSshHostGroupMembershipsUpdate
|
||||||
|
>;
|
||||||
[TableName.SshHost]: KnexOriginal.CompositeTableType<TSshHosts, TSshHostsInsert, TSshHostsUpdate>;
|
[TableName.SshHost]: KnexOriginal.CompositeTableType<TSshHosts, TSshHostsInsert, TSshHostsUpdate>;
|
||||||
[TableName.SshCertificateAuthority]: KnexOriginal.CompositeTableType<
|
[TableName.SshCertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||||
TSshCertificateAuthorities,
|
TSshCertificateAuthorities,
|
||||||
@@ -1002,6 +1028,16 @@ declare module "knex/types/tables" {
|
|||||||
TSecretRotationV2SecretMappingsInsert,
|
TSecretRotationV2SecretMappingsInsert,
|
||||||
TSecretRotationV2SecretMappingsUpdate
|
TSecretRotationV2SecretMappingsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.MicrosoftTeamsIntegrations]: KnexOriginal.CompositeTableType<
|
||||||
|
TMicrosoftTeamsIntegrations,
|
||||||
|
TMicrosoftTeamsIntegrationsInsert,
|
||||||
|
TMicrosoftTeamsIntegrationsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.ProjectMicrosoftTeamsConfigs]: KnexOriginal.CompositeTableType<
|
||||||
|
TProjectMicrosoftTeamsConfigs,
|
||||||
|
TProjectMicrosoftTeamsConfigsInsert,
|
||||||
|
TProjectMicrosoftTeamsConfigsUpdate
|
||||||
|
>;
|
||||||
[TableName.SecretReminderRecipients]: KnexOriginal.CompositeTableType<
|
[TableName.SecretReminderRecipients]: KnexOriginal.CompositeTableType<
|
||||||
TSecretReminderRecipients,
|
TSecretReminderRecipients,
|
||||||
TSecretReminderRecipientsInsert,
|
TSecretReminderRecipientsInsert,
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const superAdminHasEncryptedMicrosoftTeamsClientIdColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
"encryptedMicrosoftTeamsAppId"
|
||||||
|
);
|
||||||
|
const superAdminHasEncryptedMicrosoftTeamsClientSecret = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
"encryptedMicrosoftTeamsClientSecret"
|
||||||
|
);
|
||||||
|
const superAdminHasEncryptedMicrosoftTeamsBotId = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
"encryptedMicrosoftTeamsBotId"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!superAdminHasEncryptedMicrosoftTeamsClientIdColumn ||
|
||||||
|
!superAdminHasEncryptedMicrosoftTeamsClientSecret ||
|
||||||
|
!superAdminHasEncryptedMicrosoftTeamsBotId
|
||||||
|
) {
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (table) => {
|
||||||
|
if (!superAdminHasEncryptedMicrosoftTeamsClientIdColumn) {
|
||||||
|
table.binary("encryptedMicrosoftTeamsAppId").nullable();
|
||||||
|
}
|
||||||
|
if (!superAdminHasEncryptedMicrosoftTeamsClientSecret) {
|
||||||
|
table.binary("encryptedMicrosoftTeamsClientSecret").nullable();
|
||||||
|
}
|
||||||
|
if (!superAdminHasEncryptedMicrosoftTeamsBotId) {
|
||||||
|
table.binary("encryptedMicrosoftTeamsBotId").nullable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.WorkflowIntegrations, "status"))) {
|
||||||
|
await knex.schema.alterTable(TableName.WorkflowIntegrations, (table) => {
|
||||||
|
table.enu("status", ["pending", "installed", "failed"]).notNullable().defaultTo("installed"); // defaults to installed so we can have backwards compatibility with existing workflow integrations
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.MicrosoftTeamsIntegrations))) {
|
||||||
|
await knex.schema.createTable(TableName.MicrosoftTeamsIntegrations, (table) => {
|
||||||
|
table.uuid("id", { primaryKey: true }).notNullable();
|
||||||
|
table.foreign("id").references("id").inTable(TableName.WorkflowIntegrations).onDelete("CASCADE"); // the ID itself is the workflow integration ID
|
||||||
|
|
||||||
|
table.string("internalTeamsAppId").nullable();
|
||||||
|
table.string("tenantId").notNullable();
|
||||||
|
table.binary("encryptedAccessToken").nullable();
|
||||||
|
table.binary("encryptedBotAccessToken").nullable();
|
||||||
|
|
||||||
|
table.timestamp("accessTokenExpiresAt").nullable();
|
||||||
|
table.timestamp("botAccessTokenExpiresAt").nullable();
|
||||||
|
|
||||||
|
table.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.MicrosoftTeamsIntegrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.ProjectMicrosoftTeamsConfigs))) {
|
||||||
|
await knex.schema.createTable(TableName.ProjectMicrosoftTeamsConfigs, (tb) => {
|
||||||
|
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
tb.string("projectId").notNullable().unique();
|
||||||
|
tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
tb.uuid("microsoftTeamsIntegrationId").notNullable();
|
||||||
|
tb.foreign("microsoftTeamsIntegrationId")
|
||||||
|
.references("id")
|
||||||
|
.inTable(TableName.MicrosoftTeamsIntegrations)
|
||||||
|
.onDelete("CASCADE");
|
||||||
|
tb.boolean("isAccessRequestNotificationEnabled").notNullable().defaultTo(false);
|
||||||
|
tb.boolean("isSecretRequestNotificationEnabled").notNullable().defaultTo(false);
|
||||||
|
|
||||||
|
tb.jsonb("accessRequestChannels").notNullable(); // {teamId: string, channelIds: string[]}
|
||||||
|
tb.jsonb("secretRequestChannels").notNullable(); // {teamId: string, channelIds: string[]}
|
||||||
|
tb.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.ProjectMicrosoftTeamsConfigs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasEncryptedMicrosoftTeamsClientIdColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
"encryptedMicrosoftTeamsAppId"
|
||||||
|
);
|
||||||
|
const hasEncryptedMicrosoftTeamsClientSecret = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
"encryptedMicrosoftTeamsClientSecret"
|
||||||
|
);
|
||||||
|
const hasEncryptedMicrosoftTeamsBotId = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
"encryptedMicrosoftTeamsBotId"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasEncryptedMicrosoftTeamsClientIdColumn ||
|
||||||
|
hasEncryptedMicrosoftTeamsClientSecret ||
|
||||||
|
hasEncryptedMicrosoftTeamsBotId
|
||||||
|
) {
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (table) => {
|
||||||
|
if (hasEncryptedMicrosoftTeamsClientIdColumn) {
|
||||||
|
table.dropColumn("encryptedMicrosoftTeamsAppId");
|
||||||
|
}
|
||||||
|
if (hasEncryptedMicrosoftTeamsClientSecret) {
|
||||||
|
table.dropColumn("encryptedMicrosoftTeamsClientSecret");
|
||||||
|
}
|
||||||
|
if (hasEncryptedMicrosoftTeamsBotId) {
|
||||||
|
table.dropColumn("encryptedMicrosoftTeamsBotId");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (await knex.schema.hasColumn(TableName.WorkflowIntegrations, "status")) {
|
||||||
|
await knex.schema.alterTable(TableName.WorkflowIntegrations, (table) => {
|
||||||
|
table.dropColumn("status");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.ProjectMicrosoftTeamsConfigs)) {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.ProjectMicrosoftTeamsConfigs);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.ProjectMicrosoftTeamsConfigs);
|
||||||
|
}
|
||||||
|
if (await knex.schema.hasTable(TableName.MicrosoftTeamsIntegrations)) {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.MicrosoftTeamsIntegrations);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.MicrosoftTeamsIntegrations);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshHostGroup))) {
|
||||||
|
await knex.schema.createTable(TableName.SshHostGroup, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.string("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.string("name").notNullable();
|
||||||
|
t.unique(["projectId", "name"]);
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshHostGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshHostGroupMembership))) {
|
||||||
|
await knex.schema.createTable(TableName.SshHostGroupMembership, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshHostGroupId").notNullable();
|
||||||
|
t.foreign("sshHostGroupId").references("id").inTable(TableName.SshHostGroup).onDelete("CASCADE");
|
||||||
|
t.uuid("sshHostId").notNullable();
|
||||||
|
t.foreign("sshHostId").references("id").inTable(TableName.SshHost).onDelete("CASCADE");
|
||||||
|
t.unique(["sshHostGroupId", "sshHostId"]);
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshHostGroupMembership);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasGroupColumn = await knex.schema.hasColumn(TableName.SshHostLoginUser, "sshHostGroupId");
|
||||||
|
if (!hasGroupColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.SshHostLoginUser, (t) => {
|
||||||
|
t.uuid("sshHostGroupId").nullable();
|
||||||
|
t.foreign("sshHostGroupId").references("id").inTable(TableName.SshHostGroup).onDelete("CASCADE");
|
||||||
|
t.uuid("sshHostId").nullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasGroupColumn = await knex.schema.hasColumn(TableName.SshHostLoginUser, "sshHostGroupId");
|
||||||
|
if (hasGroupColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.SshHostLoginUser, (t) => {
|
||||||
|
t.dropColumn("sshHostGroupId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshHostGroupMembership);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshHostGroupMembership);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshHostGroup);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshHostGroup);
|
||||||
|
}
|
||||||
@@ -58,6 +58,7 @@ export * from "./kms-keys";
|
|||||||
export * from "./kms-root-config";
|
export * from "./kms-root-config";
|
||||||
export * from "./ldap-configs";
|
export * from "./ldap-configs";
|
||||||
export * from "./ldap-group-maps";
|
export * from "./ldap-group-maps";
|
||||||
|
export * from "./microsoft-teams-integrations";
|
||||||
export * from "./models";
|
export * from "./models";
|
||||||
export * from "./oidc-configs";
|
export * from "./oidc-configs";
|
||||||
export * from "./org-bots";
|
export * from "./org-bots";
|
||||||
@@ -127,6 +128,8 @@ export * from "./ssh-certificate-authority-secrets";
|
|||||||
export * from "./ssh-certificate-bodies";
|
export * from "./ssh-certificate-bodies";
|
||||||
export * from "./ssh-certificate-templates";
|
export * from "./ssh-certificate-templates";
|
||||||
export * from "./ssh-certificates";
|
export * from "./ssh-certificates";
|
||||||
|
export * from "./ssh-host-group-memberships";
|
||||||
|
export * from "./ssh-host-groups";
|
||||||
export * from "./ssh-host-login-user-mappings";
|
export * from "./ssh-host-login-user-mappings";
|
||||||
export * from "./ssh-host-login-users";
|
export * from "./ssh-host-login-users";
|
||||||
export * from "./ssh-hosts";
|
export * from "./ssh-hosts";
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// 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 { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const MicrosoftTeamsIntegrationsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
internalTeamsAppId: z.string().nullable().optional(),
|
||||||
|
tenantId: z.string(),
|
||||||
|
encryptedAccessToken: zodBuffer.nullable().optional(),
|
||||||
|
encryptedBotAccessToken: zodBuffer.nullable().optional(),
|
||||||
|
accessTokenExpiresAt: z.date().nullable().optional(),
|
||||||
|
botAccessTokenExpiresAt: z.date().nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TMicrosoftTeamsIntegrations = z.infer<typeof MicrosoftTeamsIntegrationsSchema>;
|
||||||
|
export type TMicrosoftTeamsIntegrationsInsert = Omit<
|
||||||
|
z.input<typeof MicrosoftTeamsIntegrationsSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TMicrosoftTeamsIntegrationsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof MicrosoftTeamsIntegrationsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -2,6 +2,8 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export enum TableName {
|
export enum TableName {
|
||||||
Users = "users",
|
Users = "users",
|
||||||
|
SshHostGroup = "ssh_host_groups",
|
||||||
|
SshHostGroupMembership = "ssh_host_group_memberships",
|
||||||
SshHost = "ssh_hosts",
|
SshHost = "ssh_hosts",
|
||||||
SshHostLoginUser = "ssh_host_login_users",
|
SshHostLoginUser = "ssh_host_login_users",
|
||||||
SshHostLoginUserMapping = "ssh_host_login_user_mappings",
|
SshHostLoginUserMapping = "ssh_host_login_user_mappings",
|
||||||
@@ -147,6 +149,8 @@ export enum TableName {
|
|||||||
KmipClientCertificates = "kmip_client_certificates",
|
KmipClientCertificates = "kmip_client_certificates",
|
||||||
SecretRotationV2 = "secret_rotations_v2",
|
SecretRotationV2 = "secret_rotations_v2",
|
||||||
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings",
|
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings",
|
||||||
|
MicrosoftTeamsIntegrations = "microsoft_teams_integrations",
|
||||||
|
ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs",
|
||||||
SecretReminderRecipients = "secret_reminder_recipients",
|
SecretReminderRecipients = "secret_reminder_recipients",
|
||||||
GithubOrgSyncConfig = "github_org_sync_configs"
|
GithubOrgSyncConfig = "github_org_sync_configs"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export const OrganizationsSchema = z.object({
|
|||||||
defaultMembershipRole: z.string().default("member"),
|
defaultMembershipRole: z.string().default("member"),
|
||||||
enforceMfa: z.boolean().default(false),
|
enforceMfa: z.boolean().default(false),
|
||||||
selectedMfaMethod: z.string().nullable().optional(),
|
selectedMfaMethod: z.string().nullable().optional(),
|
||||||
secretShareSendToAnyone: z.boolean().default(true).nullable().optional(),
|
|
||||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
|
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
|
||||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// 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 ProjectMicrosoftTeamsConfigsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
projectId: z.string(),
|
||||||
|
microsoftTeamsIntegrationId: z.string().uuid(),
|
||||||
|
isAccessRequestNotificationEnabled: z.boolean().default(false),
|
||||||
|
isSecretRequestNotificationEnabled: z.boolean().default(false),
|
||||||
|
accessRequestChannels: z.unknown(),
|
||||||
|
secretRequestChannels: z.unknown(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TProjectMicrosoftTeamsConfigs = z.infer<typeof ProjectMicrosoftTeamsConfigsSchema>;
|
||||||
|
export type TProjectMicrosoftTeamsConfigsInsert = Omit<
|
||||||
|
z.input<typeof ProjectMicrosoftTeamsConfigsSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TProjectMicrosoftTeamsConfigsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof ProjectMicrosoftTeamsConfigsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// 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 SshHostGroupMembershipsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
sshHostGroupId: z.string().uuid(),
|
||||||
|
sshHostId: z.string().uuid()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshHostGroupMemberships = z.infer<typeof SshHostGroupMembershipsSchema>;
|
||||||
|
export type TSshHostGroupMembershipsInsert = Omit<z.input<typeof SshHostGroupMembershipsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshHostGroupMembershipsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof SshHostGroupMembershipsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// 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 SshHostGroupsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
projectId: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshHostGroups = z.infer<typeof SshHostGroupsSchema>;
|
||||||
|
export type TSshHostGroupsInsert = Omit<z.input<typeof SshHostGroupsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshHostGroupsUpdate = Partial<Omit<z.input<typeof SshHostGroupsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -11,8 +11,9 @@ export const SshHostLoginUsersSchema = z.object({
|
|||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
sshHostId: z.string().uuid(),
|
sshHostId: z.string().uuid().nullable().optional(),
|
||||||
loginUser: z.string()
|
loginUser: z.string(),
|
||||||
|
sshHostGroupId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSshHostLoginUsers = z.infer<typeof SshHostLoginUsersSchema>;
|
export type TSshHostLoginUsers = z.infer<typeof SshHostLoginUsersSchema>;
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ export const SuperAdminSchema = z.object({
|
|||||||
encryptedSlackClientSecret: zodBuffer.nullable().optional(),
|
encryptedSlackClientSecret: zodBuffer.nullable().optional(),
|
||||||
authConsentContent: z.string().nullable().optional(),
|
authConsentContent: z.string().nullable().optional(),
|
||||||
pageFrameContent: z.string().nullable().optional(),
|
pageFrameContent: z.string().nullable().optional(),
|
||||||
adminIdentityIds: z.string().array().nullable().optional()
|
adminIdentityIds: z.string().array().nullable().optional(),
|
||||||
|
encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(),
|
||||||
|
encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(),
|
||||||
|
encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export const WorkflowIntegrationsSchema = z.object({
|
|||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
status: z.string().default("installed")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TWorkflowIntegrations = z.infer<typeof WorkflowIntegrationsSchema>;
|
export type TWorkflowIntegrations = z.infer<typeof WorkflowIntegrationsSchema>;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { registerSnapshotRouter } from "./snapshot-router";
|
|||||||
import { registerSshCaRouter } from "./ssh-certificate-authority-router";
|
import { registerSshCaRouter } from "./ssh-certificate-authority-router";
|
||||||
import { registerSshCertRouter } from "./ssh-certificate-router";
|
import { registerSshCertRouter } from "./ssh-certificate-router";
|
||||||
import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router";
|
import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router";
|
||||||
|
import { registerSshHostGroupRouter } from "./ssh-host-group-router";
|
||||||
import { registerSshHostRouter } from "./ssh-host-router";
|
import { registerSshHostRouter } from "./ssh-host-router";
|
||||||
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
||||||
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
||||||
@@ -88,6 +89,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
await sshRouter.register(registerSshCertRouter, { prefix: "/certificates" });
|
await sshRouter.register(registerSshCertRouter, { prefix: "/certificates" });
|
||||||
await sshRouter.register(registerSshCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
await sshRouter.register(registerSshCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||||
await sshRouter.register(registerSshHostRouter, { prefix: "/hosts" });
|
await sshRouter.register(registerSshHostRouter, { prefix: "/hosts" });
|
||||||
|
await sshRouter.register(registerSshHostGroupRouter, { prefix: "/host-groups" });
|
||||||
},
|
},
|
||||||
{ prefix: "/ssh" }
|
{ prefix: "/ssh" }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,360 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
|
||||||
|
import { sanitizedSshHostGroup } from "@app/ee/services/ssh-host-group/ssh-host-group-schema";
|
||||||
|
import { EHostGroupMembershipFilter } from "@app/ee/services/ssh-host-group/ssh-host-group-types";
|
||||||
|
import { ApiDocsTags, SSH_HOST_GROUPS } from "@app/lib/api-docs";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerSshHostGroupRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshHostGroupId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHostGroups],
|
||||||
|
description: "Get SSH Host Group",
|
||||||
|
params: z.object({
|
||||||
|
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.GET.sshHostGroupId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshHostGroup.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const sshHostGroup = await server.services.sshHostGroup.getSshHostGroup({
|
||||||
|
sshHostGroupId: req.params.sshHostGroupId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: sshHostGroup.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SSH_HOST_GROUP,
|
||||||
|
metadata: {
|
||||||
|
sshHostGroupId: sshHostGroup.id,
|
||||||
|
name: sshHostGroup.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return sshHostGroup;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHostGroups],
|
||||||
|
description: "Create SSH Host Group",
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string().describe(SSH_HOST_GROUPS.CREATE.projectId),
|
||||||
|
name: slugSchema({ min: 1, max: 64, field: "name" }).describe(SSH_HOST_GROUPS.CREATE.name),
|
||||||
|
loginMappings: z.array(loginMappingSchema).default([]).describe(SSH_HOST_GROUPS.CREATE.loginMappings)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshHostGroup.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const sshHostGroup = await server.services.sshHostGroup.createSshHostGroup({
|
||||||
|
...req.body,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: sshHostGroup.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_SSH_HOST_GROUP,
|
||||||
|
metadata: {
|
||||||
|
sshHostGroupId: sshHostGroup.id,
|
||||||
|
name: sshHostGroup.name,
|
||||||
|
loginMappings: sshHostGroup.loginMappings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return sshHostGroup;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:sshHostGroupId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHostGroups],
|
||||||
|
description: "Update SSH Host Group",
|
||||||
|
params: z.object({
|
||||||
|
sshHostGroupId: z.string().trim().describe(SSH_HOST_GROUPS.UPDATE.sshHostGroupId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
name: slugSchema({ min: 1, max: 64, field: "name" }).describe(SSH_HOST_GROUPS.UPDATE.name).optional(),
|
||||||
|
loginMappings: z.array(loginMappingSchema).optional().describe(SSH_HOST_GROUPS.UPDATE.loginMappings)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshHostGroup.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const sshHostGroup = await server.services.sshHostGroup.updateSshHostGroup({
|
||||||
|
sshHostGroupId: req.params.sshHostGroupId,
|
||||||
|
...req.body,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: sshHostGroup.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_SSH_HOST_GROUP,
|
||||||
|
metadata: {
|
||||||
|
sshHostGroupId: sshHostGroup.id,
|
||||||
|
name: sshHostGroup.name,
|
||||||
|
loginMappings: sshHostGroup.loginMappings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return sshHostGroup;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:sshHostGroupId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHostGroups],
|
||||||
|
description: "Delete SSH Host Group",
|
||||||
|
params: z.object({
|
||||||
|
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.DELETE.sshHostGroupId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshHostGroup.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const sshHostGroup = await server.services.sshHostGroup.deleteSshHostGroup({
|
||||||
|
sshHostGroupId: req.params.sshHostGroupId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: sshHostGroup.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_SSH_HOST_GROUP,
|
||||||
|
metadata: {
|
||||||
|
sshHostGroupId: sshHostGroup.id,
|
||||||
|
name: sshHostGroup.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return sshHostGroup;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshHostGroupId/hosts",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHostGroups],
|
||||||
|
description: "Get SSH Hosts in a Host Group",
|
||||||
|
params: z.object({
|
||||||
|
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.GET.sshHostGroupId)
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
filter: z.nativeEnum(EHostGroupMembershipFilter).optional().describe(SSH_HOST_GROUPS.GET.filter)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
hosts: sanitizedSshHost
|
||||||
|
.pick({
|
||||||
|
id: true,
|
||||||
|
hostname: true,
|
||||||
|
alias: true
|
||||||
|
})
|
||||||
|
.merge(
|
||||||
|
z.object({
|
||||||
|
isPartOfGroup: z.boolean(),
|
||||||
|
joinedGroupAt: z.date().nullable()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.array(),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { sshHostGroup, hosts, totalCount } = await server.services.sshHostGroup.listSshHostGroupHosts({
|
||||||
|
sshHostGroupId: req.params.sshHostGroupId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.query
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: sshHostGroup.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SSH_HOST_GROUP_HOSTS,
|
||||||
|
metadata: {
|
||||||
|
sshHostGroupId: req.params.sshHostGroupId,
|
||||||
|
name: sshHostGroup.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { hosts, totalCount };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:sshHostGroupId/hosts/:hostId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHostGroups],
|
||||||
|
description: "Add an SSH Host to a Host Group",
|
||||||
|
params: z.object({
|
||||||
|
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.ADD_HOST.sshHostGroupId),
|
||||||
|
hostId: z.string().describe(SSH_HOST_GROUPS.ADD_HOST.hostId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshHost.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { sshHostGroup, sshHost } = await server.services.sshHostGroup.addHostToSshHostGroup({
|
||||||
|
sshHostGroupId: req.params.sshHostGroupId,
|
||||||
|
hostId: req.params.hostId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: sshHost.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.ADD_HOST_TO_SSH_HOST_GROUP,
|
||||||
|
metadata: {
|
||||||
|
sshHostGroupId: sshHostGroup.id,
|
||||||
|
sshHostId: sshHost.id,
|
||||||
|
hostname: sshHost.hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return sshHost;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:sshHostGroupId/hosts/:hostId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHostGroups],
|
||||||
|
description: "Remove an SSH Host from a Host Group",
|
||||||
|
params: z.object({
|
||||||
|
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.DELETE_HOST.sshHostGroupId),
|
||||||
|
hostId: z.string().describe(SSH_HOST_GROUPS.DELETE_HOST.hostId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshHost.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { sshHostGroup, sshHost } = await server.services.sshHostGroup.removeHostFromSshHostGroup({
|
||||||
|
sshHostGroupId: req.params.sshHostGroupId,
|
||||||
|
hostId: req.params.hostId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: sshHost.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.REMOVE_HOST_FROM_SSH_HOST_GROUP,
|
||||||
|
metadata: {
|
||||||
|
sshHostGroupId: sshHostGroup.id,
|
||||||
|
sshHostId: sshHost.id,
|
||||||
|
hostname: sshHost.hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return sshHost;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -3,8 +3,9 @@ import { z } from "zod";
|
|||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
|
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
|
||||||
|
import { LoginMappingSource } from "@app/ee/services/ssh-host/ssh-host-types";
|
||||||
import { isValidHostname } from "@app/ee/services/ssh-host/ssh-host-validators";
|
import { isValidHostname } from "@app/ee/services/ssh-host/ssh-host-validators";
|
||||||
import { SSH_HOSTS } from "@app/lib/api-docs";
|
import { ApiDocsTags, SSH_HOSTS } from "@app/lib/api-docs";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { publicSshCaLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { publicSshCaLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
@@ -21,10 +22,16 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHosts],
|
||||||
response: {
|
response: {
|
||||||
200: z.array(
|
200: z.array(
|
||||||
sanitizedSshHost.extend({
|
sanitizedSshHost.extend({
|
||||||
loginMappings: z.array(loginMappingSchema)
|
loginMappings: loginMappingSchema
|
||||||
|
.extend({
|
||||||
|
source: z.nativeEnum(LoginMappingSource)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -49,12 +56,18 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHosts],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
sshHostId: z.string().describe(SSH_HOSTS.GET.sshHostId)
|
sshHostId: z.string().describe(SSH_HOSTS.GET.sshHostId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: sanitizedSshHost.extend({
|
200: sanitizedSshHost.extend({
|
||||||
loginMappings: z.array(loginMappingSchema)
|
loginMappings: loginMappingSchema
|
||||||
|
.extend({
|
||||||
|
source: z.nativeEnum(LoginMappingSource)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -91,7 +104,9 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
description: "Add an SSH Host",
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHosts],
|
||||||
|
description: "Register SSH Host",
|
||||||
body: z.object({
|
body: z.object({
|
||||||
projectId: z.string().describe(SSH_HOSTS.CREATE.projectId),
|
projectId: z.string().describe(SSH_HOSTS.CREATE.projectId),
|
||||||
hostname: z
|
hostname: z
|
||||||
@@ -119,7 +134,11 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: sanitizedSshHost.extend({
|
200: sanitizedSshHost.extend({
|
||||||
loginMappings: z.array(loginMappingSchema)
|
loginMappings: loginMappingSchema
|
||||||
|
.extend({
|
||||||
|
source: z.nativeEnum(LoginMappingSource)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -163,6 +182,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHosts],
|
||||||
description: "Update SSH Host",
|
description: "Update SSH Host",
|
||||||
params: z.object({
|
params: z.object({
|
||||||
sshHostId: z.string().trim().describe(SSH_HOSTS.UPDATE.sshHostId)
|
sshHostId: z.string().trim().describe(SSH_HOSTS.UPDATE.sshHostId)
|
||||||
@@ -192,7 +213,11 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: sanitizedSshHost.extend({
|
200: sanitizedSshHost.extend({
|
||||||
loginMappings: z.array(loginMappingSchema)
|
loginMappings: loginMappingSchema
|
||||||
|
.extend({
|
||||||
|
source: z.nativeEnum(LoginMappingSource)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -235,12 +260,19 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHosts],
|
||||||
|
description: "Delete SSH Host",
|
||||||
params: z.object({
|
params: z.object({
|
||||||
sshHostId: z.string().describe(SSH_HOSTS.DELETE.sshHostId)
|
sshHostId: z.string().describe(SSH_HOSTS.DELETE.sshHostId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: sanitizedSshHost.extend({
|
200: sanitizedSshHost.extend({
|
||||||
loginMappings: z.array(loginMappingSchema)
|
loginMappings: loginMappingSchema
|
||||||
|
.extend({
|
||||||
|
source: z.nativeEnum(LoginMappingSource)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -278,6 +310,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHosts],
|
||||||
description: "Issue SSH certificate for user",
|
description: "Issue SSH certificate for user",
|
||||||
params: z.object({
|
params: z.object({
|
||||||
sshHostId: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.sshHostId)
|
sshHostId: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.sshHostId)
|
||||||
@@ -350,6 +384,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHosts],
|
||||||
description: "Issue SSH certificate for host",
|
description: "Issue SSH certificate for host",
|
||||||
params: z.object({
|
params: z.object({
|
||||||
sshHostId: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.sshHostId)
|
sshHostId: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.sshHostId)
|
||||||
@@ -414,6 +450,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: publicSshCaLimit
|
rateLimit: publicSshCaLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHosts],
|
||||||
description: "Get public key of the user SSH CA linked to the host",
|
description: "Get public key of the user SSH CA linked to the host",
|
||||||
params: z.object({
|
params: z.object({
|
||||||
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.sshHostId)
|
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.sshHostId)
|
||||||
@@ -435,6 +473,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: publicSshCaLimit
|
rateLimit: publicSshCaLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHosts],
|
||||||
description: "Get public key of the host SSH CA linked to the host",
|
description: "Get public key of the host SSH CA linked to the host",
|
||||||
params: z.object({
|
params: z.object({
|
||||||
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.sshHostId)
|
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.sshHostId)
|
||||||
|
|||||||
+32
-20
@@ -6,13 +6,15 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
|
||||||
|
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||||
|
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||||
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
|
|
||||||
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
|
|
||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
@@ -67,6 +69,8 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||||
|
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
||||||
@@ -84,6 +88,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
smtpService,
|
smtpService,
|
||||||
userDAL,
|
userDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
|
microsoftTeamsService,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
projectSlackConfigDAL
|
projectSlackConfigDAL
|
||||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||||
const createAccessApprovalRequest = async ({
|
const createAccessApprovalRequest = async ({
|
||||||
@@ -219,24 +225,30 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||||
const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`;
|
const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`;
|
||||||
|
|
||||||
await triggerSlackNotification({
|
await triggerWorkflowIntegrationNotification({
|
||||||
projectId: project.id,
|
input: {
|
||||||
projectSlackConfigDAL,
|
notification: {
|
||||||
projectDAL,
|
type: TriggerFeature.ACCESS_REQUEST,
|
||||||
kmsService,
|
payload: {
|
||||||
notification: {
|
projectName: project.name,
|
||||||
type: SlackTriggerFeature.ACCESS_REQUEST,
|
requesterFullName,
|
||||||
payload: {
|
isTemporary,
|
||||||
projectName: project.name,
|
requesterEmail: requestedByUser.email as string,
|
||||||
requesterFullName,
|
secretPath,
|
||||||
isTemporary,
|
environment: envSlug,
|
||||||
requesterEmail: requestedByUser.email as string,
|
permissions: accessTypes,
|
||||||
secretPath,
|
approvalUrl,
|
||||||
environment: envSlug,
|
note
|
||||||
permissions: accessTypes,
|
}
|
||||||
approvalUrl,
|
},
|
||||||
note
|
projectId: project.id
|
||||||
}
|
},
|
||||||
|
dependencies: {
|
||||||
|
projectDAL,
|
||||||
|
projectSlackConfigDAL,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsService,
|
||||||
|
projectMicrosoftTeamsConfigDAL
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||||
|
import { TLoginMapping } from "@app/ee/services/ssh-host/ssh-host-types";
|
||||||
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types";
|
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
TSecretSyncRaw,
|
TSecretSyncRaw,
|
||||||
TUpdateSecretSyncDTO
|
TUpdateSecretSyncDTO
|
||||||
} from "@app/services/secret-sync/secret-sync-types";
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
|
||||||
|
|
||||||
import { KmipPermission } from "../kmip/kmip-enum";
|
import { KmipPermission } from "../kmip/kmip-enum";
|
||||||
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
|
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
|
||||||
@@ -191,12 +193,19 @@ export enum EventType {
|
|||||||
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
|
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
|
||||||
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
|
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
|
||||||
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
|
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
|
||||||
|
GET_SSH_HOST = "get-ssh-host",
|
||||||
CREATE_SSH_HOST = "create-ssh-host",
|
CREATE_SSH_HOST = "create-ssh-host",
|
||||||
UPDATE_SSH_HOST = "update-ssh-host",
|
UPDATE_SSH_HOST = "update-ssh-host",
|
||||||
DELETE_SSH_HOST = "delete-ssh-host",
|
DELETE_SSH_HOST = "delete-ssh-host",
|
||||||
GET_SSH_HOST = "get-ssh-host",
|
|
||||||
ISSUE_SSH_HOST_USER_CERT = "issue-ssh-host-user-cert",
|
ISSUE_SSH_HOST_USER_CERT = "issue-ssh-host-user-cert",
|
||||||
ISSUE_SSH_HOST_HOST_CERT = "issue-ssh-host-host-cert",
|
ISSUE_SSH_HOST_HOST_CERT = "issue-ssh-host-host-cert",
|
||||||
|
GET_SSH_HOST_GROUP = "get-ssh-host-group",
|
||||||
|
CREATE_SSH_HOST_GROUP = "create-ssh-host-group",
|
||||||
|
UPDATE_SSH_HOST_GROUP = "update-ssh-host-group",
|
||||||
|
DELETE_SSH_HOST_GROUP = "delete-ssh-host-group",
|
||||||
|
GET_SSH_HOST_GROUP_HOSTS = "get-ssh-host-group-hosts",
|
||||||
|
ADD_HOST_TO_SSH_HOST_GROUP = "add-host-to-ssh-host-group",
|
||||||
|
REMOVE_HOST_FROM_SSH_HOST_GROUP = "remove-host-from-ssh-host-group",
|
||||||
CREATE_CA = "create-certificate-authority",
|
CREATE_CA = "create-certificate-authority",
|
||||||
GET_CA = "get-certificate-authority",
|
GET_CA = "get-certificate-authority",
|
||||||
UPDATE_CA = "update-certificate-authority",
|
UPDATE_CA = "update-certificate-authority",
|
||||||
@@ -244,11 +253,14 @@ export enum EventType {
|
|||||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
||||||
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
||||||
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
||||||
|
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||||
|
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||||
GET_SLACK_INTEGRATION = "get-slack-integration",
|
GET_SLACK_INTEGRATION = "get-slack-integration",
|
||||||
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
|
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
|
||||||
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
||||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "get-project-workflow-integration-config",
|
||||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "update-project-workflow-integration-config",
|
||||||
|
|
||||||
GET_PROJECT_SSH_CONFIG = "get-project-ssh-config",
|
GET_PROJECT_SSH_CONFIG = "get-project-ssh-config",
|
||||||
UPDATE_PROJECT_SSH_CONFIG = "update-project-ssh-config",
|
UPDATE_PROJECT_SSH_CONFIG = "update-project-ssh-config",
|
||||||
INTEGRATION_SYNCED = "integration-synced",
|
INTEGRATION_SYNCED = "integration-synced",
|
||||||
@@ -321,6 +333,15 @@ export enum EventType {
|
|||||||
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
|
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
|
||||||
|
|
||||||
PROJECT_ACCESS_REQUEST = "project-access-request",
|
PROJECT_ACCESS_REQUEST = "project-access-request",
|
||||||
|
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE = "microsoft-teams-workflow-integration-create",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE = "microsoft-teams-workflow-integration-delete",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE = "microsoft-teams-workflow-integration-update",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS = "microsoft-teams-workflow-integration-check-installation-status",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS = "microsoft-teams-workflow-integration-get-teams",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
|
||||||
|
|
||||||
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
|
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
|
||||||
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
|
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
|
||||||
}
|
}
|
||||||
@@ -1499,12 +1520,7 @@ interface CreateSshHost {
|
|||||||
alias: string | null;
|
alias: string | null;
|
||||||
userCertTtl: string;
|
userCertTtl: string;
|
||||||
hostCertTtl: string;
|
hostCertTtl: string;
|
||||||
loginMappings: {
|
loginMappings: TLoginMapping[];
|
||||||
loginUser: string;
|
|
||||||
allowedPrincipals: {
|
|
||||||
usernames: string[];
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
userSshCaId: string;
|
userSshCaId: string;
|
||||||
hostSshCaId: string;
|
hostSshCaId: string;
|
||||||
};
|
};
|
||||||
@@ -1518,12 +1534,7 @@ interface UpdateSshHost {
|
|||||||
alias?: string | null;
|
alias?: string | null;
|
||||||
userCertTtl?: string;
|
userCertTtl?: string;
|
||||||
hostCertTtl?: string;
|
hostCertTtl?: string;
|
||||||
loginMappings?: {
|
loginMappings?: TLoginMapping[];
|
||||||
loginUser: string;
|
|
||||||
allowedPrincipals: {
|
|
||||||
usernames: string[];
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
userSshCaId?: string;
|
userSshCaId?: string;
|
||||||
hostSshCaId?: string;
|
hostSshCaId?: string;
|
||||||
};
|
};
|
||||||
@@ -1567,6 +1578,66 @@ interface IssueSshHostHostCert {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetSshHostGroupEvent {
|
||||||
|
type: EventType.GET_SSH_HOST_GROUP;
|
||||||
|
metadata: {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateSshHostGroupEvent {
|
||||||
|
type: EventType.CREATE_SSH_HOST_GROUP;
|
||||||
|
metadata: {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
name: string;
|
||||||
|
loginMappings: TLoginMapping[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateSshHostGroupEvent {
|
||||||
|
type: EventType.UPDATE_SSH_HOST_GROUP;
|
||||||
|
metadata: {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
name?: string;
|
||||||
|
loginMappings?: TLoginMapping[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteSshHostGroupEvent {
|
||||||
|
type: EventType.DELETE_SSH_HOST_GROUP;
|
||||||
|
metadata: {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSshHostGroupHostsEvent {
|
||||||
|
type: EventType.GET_SSH_HOST_GROUP_HOSTS;
|
||||||
|
metadata: {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddHostToSshHostGroupEvent {
|
||||||
|
type: EventType.ADD_HOST_TO_SSH_HOST_GROUP;
|
||||||
|
metadata: {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
sshHostId: string;
|
||||||
|
hostname: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RemoveHostFromSshHostGroupEvent {
|
||||||
|
type: EventType.REMOVE_HOST_FROM_SSH_HOST_GROUP;
|
||||||
|
metadata: {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
sshHostId: string;
|
||||||
|
hostname: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateCa {
|
interface CreateCa {
|
||||||
type: EventType.CREATE_CA;
|
type: EventType.CREATE_CA;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -1980,22 +2051,24 @@ interface GetSlackIntegration {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateProjectSlackConfig {
|
interface UpdateProjectWorkflowIntegrationConfig {
|
||||||
type: EventType.UPDATE_PROJECT_SLACK_CONFIG;
|
type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
|
||||||
metadata: {
|
metadata: {
|
||||||
id: string;
|
id: string;
|
||||||
slackIntegrationId: string;
|
integrationId: string;
|
||||||
|
integration: WorkflowIntegration;
|
||||||
isAccessRequestNotificationEnabled: boolean;
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
accessRequestChannels: string;
|
accessRequestChannels?: string | { teamId: string; channelIds: string[] };
|
||||||
isSecretRequestNotificationEnabled: boolean;
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
secretRequestChannels: string;
|
secretRequestChannels?: string | { teamId: string; channelIds: string[] };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetProjectSlackConfig {
|
interface GetProjectWorkflowIntegrationConfig {
|
||||||
type: EventType.GET_PROJECT_SLACK_CONFIG;
|
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
|
||||||
metadata: {
|
metadata: {
|
||||||
id: string;
|
id: string;
|
||||||
|
integration: WorkflowIntegration;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2561,6 +2634,66 @@ interface RotateSecretRotationEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MicrosoftTeamsWorkflowIntegrationCreateEvent {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE;
|
||||||
|
metadata: {
|
||||||
|
tenantId: string;
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MicrosoftTeamsWorkflowIntegrationDeleteEvent {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE;
|
||||||
|
metadata: {
|
||||||
|
tenantId: string;
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS;
|
||||||
|
metadata: {
|
||||||
|
tenantId: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MicrosoftTeamsWorkflowIntegrationGetTeamsEvent {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS;
|
||||||
|
metadata: {
|
||||||
|
tenantId: string;
|
||||||
|
slug: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MicrosoftTeamsWorkflowIntegrationGetEvent {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET;
|
||||||
|
metadata: {
|
||||||
|
tenantId: string;
|
||||||
|
slug: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MicrosoftTeamsWorkflowIntegrationListEvent {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST;
|
||||||
|
metadata: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MicrosoftTeamsWorkflowIntegrationUpdateEvent {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE;
|
||||||
|
metadata: {
|
||||||
|
tenantId: string;
|
||||||
|
slug: string;
|
||||||
|
id: string;
|
||||||
|
newSlug?: string;
|
||||||
|
newDescription?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -2723,8 +2856,8 @@ export type Event =
|
|||||||
| UpdateSlackIntegration
|
| UpdateSlackIntegration
|
||||||
| DeleteSlackIntegration
|
| DeleteSlackIntegration
|
||||||
| GetSlackIntegration
|
| GetSlackIntegration
|
||||||
| UpdateProjectSlackConfig
|
| UpdateProjectWorkflowIntegrationConfig
|
||||||
| GetProjectSlackConfig
|
| GetProjectWorkflowIntegrationConfig
|
||||||
| GetProjectSshConfig
|
| GetProjectSshConfig
|
||||||
| UpdateProjectSshConfig
|
| UpdateProjectSshConfig
|
||||||
| IntegrationSyncedEvent
|
| IntegrationSyncedEvent
|
||||||
@@ -2753,6 +2886,13 @@ export type Event =
|
|||||||
| CreateAppConnectionEvent
|
| CreateAppConnectionEvent
|
||||||
| UpdateAppConnectionEvent
|
| UpdateAppConnectionEvent
|
||||||
| DeleteAppConnectionEvent
|
| DeleteAppConnectionEvent
|
||||||
|
| GetSshHostGroupEvent
|
||||||
|
| CreateSshHostGroupEvent
|
||||||
|
| UpdateSshHostGroupEvent
|
||||||
|
| DeleteSshHostGroupEvent
|
||||||
|
| GetSshHostGroupHostsEvent
|
||||||
|
| AddHostToSshHostGroupEvent
|
||||||
|
| RemoveHostFromSshHostGroupEvent
|
||||||
| CreateSharedSecretEvent
|
| CreateSharedSecretEvent
|
||||||
| DeleteSharedSecretEvent
|
| DeleteSharedSecretEvent
|
||||||
| ReadSharedSecretEvent
|
| ReadSharedSecretEvent
|
||||||
@@ -2794,4 +2934,11 @@ export type Event =
|
|||||||
| CreateSecretRotationEvent
|
| CreateSecretRotationEvent
|
||||||
| UpdateSecretRotationEvent
|
| UpdateSecretRotationEvent
|
||||||
| DeleteSecretRotationEvent
|
| DeleteSecretRotationEvent
|
||||||
| RotateSecretRotationEvent;
|
| RotateSecretRotationEvent
|
||||||
|
| MicrosoftTeamsWorkflowIntegrationCreateEvent
|
||||||
|
| MicrosoftTeamsWorkflowIntegrationDeleteEvent
|
||||||
|
| MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent
|
||||||
|
| MicrosoftTeamsWorkflowIntegrationGetTeamsEvent
|
||||||
|
| MicrosoftTeamsWorkflowIntegrationGetEvent
|
||||||
|
| MicrosoftTeamsWorkflowIntegrationListEvent
|
||||||
|
| MicrosoftTeamsWorkflowIntegrationUpdateEvent;
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export const groupDALFactory = (db: TDbClient) => {
|
|||||||
totalCount: Number(members?.[0]?.total_count ?? 0)
|
totalCount: Number(members?.[0]?.total_count ?? 0)
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find all org members" });
|
throw new DatabaseError({ error, name: "Find all user group members" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ export const getDefaultOnPremFeatures = () => {
|
|||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
secretApproval: true,
|
secretApproval: true,
|
||||||
secretRotation: true,
|
secretRotation: true,
|
||||||
caCrl: false
|
caCrl: false,
|
||||||
|
sshHostGroups: false
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const BillingPlanRows = {
|
|||||||
CustomAlerts: { name: "Custom alerts", field: "customAlerts" },
|
CustomAlerts: { name: "Custom alerts", field: "customAlerts" },
|
||||||
AuditLogs: { name: "Audit logs", field: "auditLogs" },
|
AuditLogs: { name: "Audit logs", field: "auditLogs" },
|
||||||
SamlSSO: { name: "SAML SSO", field: "samlSSO" },
|
SamlSSO: { name: "SAML SSO", field: "samlSSO" },
|
||||||
|
SshHostGroups: { name: "SSH Host Groups", field: "sshHostGroups" },
|
||||||
Hsm: { name: "Hardware Security Module (HSM)", field: "hsm" },
|
Hsm: { name: "Hardware Security Module (HSM)", field: "hsm" },
|
||||||
OidcSSO: { name: "OIDC SSO", field: "oidcSSO" },
|
OidcSSO: { name: "OIDC SSO", field: "oidcSSO" },
|
||||||
SecretApproval: { name: "Secret approvals", field: "secretApproval" },
|
SecretApproval: { name: "Secret approvals", field: "secretApproval" },
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
enforceMfa: false,
|
enforceMfa: false,
|
||||||
projectTemplates: false,
|
projectTemplates: false,
|
||||||
kmip: false,
|
kmip: false,
|
||||||
gateway: false
|
gateway: false,
|
||||||
|
sshHostGroups: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export type TFeatureSet = {
|
|||||||
projectTemplates: false;
|
projectTemplates: false;
|
||||||
kmip: false;
|
kmip: false;
|
||||||
gateway: false;
|
gateway: false;
|
||||||
|
sshHostGroups: false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgPlansTableDTO = {
|
export type TOrgPlansTableDTO = {
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ export enum ProjectPermissionSub {
|
|||||||
SshCertificates = "ssh-certificates",
|
SshCertificates = "ssh-certificates",
|
||||||
SshCertificateTemplates = "ssh-certificate-templates",
|
SshCertificateTemplates = "ssh-certificate-templates",
|
||||||
SshHosts = "ssh-hosts",
|
SshHosts = "ssh-hosts",
|
||||||
|
SshHostGroups = "ssh-host-groups",
|
||||||
PkiAlerts = "pki-alerts",
|
PkiAlerts = "pki-alerts",
|
||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
@@ -240,6 +241,7 @@ export type ProjectPermissionSet =
|
|||||||
ProjectPermissionSshHostActions,
|
ProjectPermissionSshHostActions,
|
||||||
ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
|
ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
|
||||||
]
|
]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
||||||
@@ -508,6 +510,12 @@ const GeneralPermissionSchema = [
|
|||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.SshHostGroups).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
@@ -686,7 +694,8 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionSub.PkiCollections,
|
ProjectPermissionSub.PkiCollections,
|
||||||
ProjectPermissionSub.SshCertificateAuthorities,
|
ProjectPermissionSub.SshCertificateAuthorities,
|
||||||
ProjectPermissionSub.SshCertificates,
|
ProjectPermissionSub.SshCertificates,
|
||||||
ProjectPermissionSub.SshCertificateTemplates
|
ProjectPermissionSub.SshCertificateTemplates,
|
||||||
|
ProjectPermissionSub.SshHostGroups
|
||||||
].forEach((el) => {
|
].forEach((el) => {
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
|
|||||||
+51
-31
@@ -17,9 +17,13 @@ import { groupBy, pick, unique } from "@app/lib/fn";
|
|||||||
import { setKnexStringValue } from "@app/lib/knex";
|
import { setKnexStringValue } from "@app/lib/knex";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { EnforcementLevel } from "@app/lib/types";
|
import { EnforcementLevel } from "@app/lib/types";
|
||||||
|
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
|
||||||
|
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||||
|
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
@@ -52,8 +56,6 @@ import {
|
|||||||
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||||
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||||
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
|
|
||||||
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
|
|
||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
@@ -126,6 +128,8 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
|
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
|
||||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
||||||
@@ -155,7 +159,9 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
|
microsoftTeamsService
|
||||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
@@ -1171,21 +1177,28 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||||
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
||||||
await triggerSlackNotification({
|
|
||||||
projectId,
|
await triggerWorkflowIntegrationNotification({
|
||||||
projectDAL,
|
input: {
|
||||||
kmsService,
|
projectId,
|
||||||
projectSlackConfigDAL,
|
notification: {
|
||||||
notification: {
|
type: TriggerFeature.SECRET_APPROVAL,
|
||||||
type: SlackTriggerFeature.SECRET_APPROVAL,
|
payload: {
|
||||||
payload: {
|
userEmail: user.email as string,
|
||||||
userEmail: user.email as string,
|
environment: env.name,
|
||||||
environment: env.name,
|
secretPath,
|
||||||
secretPath,
|
projectId,
|
||||||
projectId,
|
requestId: secretApprovalRequest.id,
|
||||||
requestId: secretApprovalRequest.id,
|
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
|
||||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
projectDAL,
|
||||||
|
projectSlackConfigDAL,
|
||||||
|
kmsService,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
|
microsoftTeamsService
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1503,21 +1516,28 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
||||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||||
await triggerSlackNotification({
|
|
||||||
projectId,
|
await triggerWorkflowIntegrationNotification({
|
||||||
projectDAL,
|
input: {
|
||||||
kmsService,
|
projectId,
|
||||||
projectSlackConfigDAL,
|
notification: {
|
||||||
notification: {
|
type: TriggerFeature.SECRET_APPROVAL,
|
||||||
type: SlackTriggerFeature.SECRET_APPROVAL,
|
payload: {
|
||||||
payload: {
|
userEmail: user.email as string,
|
||||||
userEmail: user.email as string,
|
environment: env.name,
|
||||||
environment: env.name,
|
secretPath,
|
||||||
secretPath,
|
projectId,
|
||||||
projectId,
|
requestId: secretApprovalRequest.id,
|
||||||
requestId: secretApprovalRequest.id,
|
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
|
||||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
projectSlackConfigDAL,
|
||||||
|
microsoftTeamsService,
|
||||||
|
projectMicrosoftTeamsConfigDAL
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export const parseRotationErrorMessage = (err: unknown): string => {
|
|||||||
if (err instanceof AxiosError) {
|
if (err instanceof AxiosError) {
|
||||||
errorMessage += err?.response?.data
|
errorMessage += err?.response?.data
|
||||||
? JSON.stringify(err?.response?.data)
|
? JSON.stringify(err?.response?.data)
|
||||||
: err?.message ?? "An unknown error occurred.";
|
: (err?.message ?? "An unknown error occurred.");
|
||||||
} else {
|
} else {
|
||||||
errorMessage += (err as Error)?.message || "An unknown error occurred.";
|
errorMessage += (err as Error)?.message || "An unknown error occurred.";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const BaseSecretRotationSchema = (type: SecretRotation) =>
|
|||||||
// unique to provider
|
// unique to provider
|
||||||
type: true,
|
type: true,
|
||||||
parameters: true,
|
parameters: true,
|
||||||
secretMappings: true
|
secretsMapping: true
|
||||||
}).extend({
|
}).extend({
|
||||||
connection: z.object({
|
connection: z.object({
|
||||||
app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]),
|
app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]),
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
|
import { groupBy, unique } from "@app/lib/fn";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
import { EHostGroupMembershipFilter } from "./ssh-host-group-types";
|
||||||
|
|
||||||
|
export type TSshHostGroupDALFactory = ReturnType<typeof sshHostGroupDALFactory>;
|
||||||
|
|
||||||
|
export const sshHostGroupDALFactory = (db: TDbClient) => {
|
||||||
|
const sshHostGroupOrm = ormify(db, TableName.SshHostGroup);
|
||||||
|
|
||||||
|
const findSshHostGroupsWithLoginMappings = async (projectId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
// First, get all the SSH host groups with their login mappings
|
||||||
|
const rows = await (tx || db.replicaNode())(TableName.SshHostGroup)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SshHostLoginUser,
|
||||||
|
`${TableName.SshHostGroup}.id`,
|
||||||
|
`${TableName.SshHostLoginUser}.sshHostGroupId`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SshHostLoginUserMapping,
|
||||||
|
`${TableName.SshHostLoginUser}.id`,
|
||||||
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||||
|
.where(`${TableName.SshHostGroup}.projectId`, projectId)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.SshHostGroup).as("sshHostGroupId"),
|
||||||
|
db.ref("projectId").withSchema(TableName.SshHostGroup),
|
||||||
|
db.ref("name").withSchema(TableName.SshHostGroup),
|
||||||
|
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||||
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||||
|
)
|
||||||
|
.orderBy(`${TableName.SshHostGroup}.updatedAt`, "desc");
|
||||||
|
|
||||||
|
const hostsGrouped = groupBy(rows, (r) => r.sshHostGroupId);
|
||||||
|
|
||||||
|
const hostGroupIds = Object.keys(hostsGrouped);
|
||||||
|
|
||||||
|
type HostCountRow = {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
host_count: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hostCountsQuery = (await (tx ||
|
||||||
|
db
|
||||||
|
.replicaNode()(TableName.SshHostGroupMembership)
|
||||||
|
.select(`${TableName.SshHostGroupMembership}.sshHostGroupId`, db.raw(`count(*) as host_count`))
|
||||||
|
.whereIn(`${TableName.SshHostGroupMembership}.sshHostGroupId`, hostGroupIds)
|
||||||
|
.groupBy(`${TableName.SshHostGroupMembership}.sshHostGroupId`))) as HostCountRow[];
|
||||||
|
|
||||||
|
const hostCountsMap = hostCountsQuery.reduce<Record<string, number>>((acc, { sshHostGroupId, host_count }) => {
|
||||||
|
acc[sshHostGroupId] = Number(host_count);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return Object.values(hostsGrouped).map((hostRows) => {
|
||||||
|
const { sshHostGroupId, name } = hostRows[0];
|
||||||
|
const loginMappingGrouped = groupBy(
|
||||||
|
hostRows.filter((r) => r.loginUser),
|
||||||
|
(r) => r.loginUser
|
||||||
|
);
|
||||||
|
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||||
|
loginUser,
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||||
|
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
id: sshHostGroupId,
|
||||||
|
projectId,
|
||||||
|
name,
|
||||||
|
loginMappings,
|
||||||
|
hostCount: hostCountsMap[sshHostGroupId] ?? 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.SshHostGroup}: FindSshHostGroupsWithLoginMappings` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findSshHostGroupByIdWithLoginMappings = async (sshHostGroupId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const rows = await (tx || db.replicaNode())(TableName.SshHostGroup)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SshHostLoginUser,
|
||||||
|
`${TableName.SshHostGroup}.id`,
|
||||||
|
`${TableName.SshHostLoginUser}.sshHostGroupId`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SshHostLoginUserMapping,
|
||||||
|
`${TableName.SshHostLoginUser}.id`,
|
||||||
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||||
|
.where(`${TableName.SshHostGroup}.id`, sshHostGroupId)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.SshHostGroup).as("sshHostGroupId"),
|
||||||
|
db.ref("projectId").withSchema(TableName.SshHostGroup),
|
||||||
|
db.ref("name").withSchema(TableName.SshHostGroup),
|
||||||
|
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||||
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rows.length === 0) return null;
|
||||||
|
|
||||||
|
const { sshHostGroupId: id, projectId, name } = rows[0];
|
||||||
|
|
||||||
|
const loginMappingGrouped = groupBy(
|
||||||
|
rows.filter((r) => r.loginUser),
|
||||||
|
(r) => r.loginUser
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||||
|
loginUser,
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||||
|
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
projectId,
|
||||||
|
name,
|
||||||
|
loginMappings
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.SshHostGroup}: FindSshHostGroupByIdWithLoginMappings` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAllSshHostsInGroup = async ({
|
||||||
|
sshHostGroupId,
|
||||||
|
offset = 0,
|
||||||
|
limit,
|
||||||
|
filter
|
||||||
|
}: {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
filter?: EHostGroupMembershipFilter;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const sshHostGroup = await db
|
||||||
|
.replicaNode()(TableName.SshHostGroup)
|
||||||
|
.where(`${TableName.SshHostGroup}.id`, sshHostGroupId)
|
||||||
|
.select("projectId")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!sshHostGroup) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `SSH host group with ID ${sshHostGroupId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = db
|
||||||
|
.replicaNode()(TableName.SshHost)
|
||||||
|
.where(`${TableName.SshHost}.projectId`, sshHostGroup.projectId)
|
||||||
|
.leftJoin(TableName.SshHostGroupMembership, (bd) => {
|
||||||
|
bd.on(`${TableName.SshHostGroupMembership}.sshHostId`, "=", `${TableName.SshHost}.id`).andOn(
|
||||||
|
`${TableName.SshHostGroupMembership}.sshHostGroupId`,
|
||||||
|
"=",
|
||||||
|
db.raw("?", [sshHostGroupId])
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostname").withSchema(TableName.SshHost),
|
||||||
|
db.ref("alias").withSchema(TableName.SshHost),
|
||||||
|
db.ref("sshHostGroupId").withSchema(TableName.SshHostGroupMembership),
|
||||||
|
db.ref("createdAt").withSchema(TableName.SshHostGroupMembership).as("joinedGroupAt"),
|
||||||
|
db.raw(`count(*) OVER() as total_count`)
|
||||||
|
)
|
||||||
|
.offset(offset)
|
||||||
|
.orderBy(`${TableName.SshHost}.hostname`, "asc");
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
void query.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
switch (filter) {
|
||||||
|
case EHostGroupMembershipFilter.GROUP_MEMBERS:
|
||||||
|
void query.andWhere(`${TableName.SshHostGroupMembership}.createdAt`, "is not", null);
|
||||||
|
break;
|
||||||
|
case EHostGroupMembershipFilter.NON_GROUP_MEMBERS:
|
||||||
|
void query.andWhere(`${TableName.SshHostGroupMembership}.createdAt`, "is", null);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hosts = await query;
|
||||||
|
|
||||||
|
return {
|
||||||
|
hosts: hosts.map(({ id, hostname, alias, sshHostGroupId: memberGroupId, joinedGroupAt }) => ({
|
||||||
|
id,
|
||||||
|
hostname,
|
||||||
|
alias,
|
||||||
|
isPartOfGroup: !!memberGroupId,
|
||||||
|
joinedGroupAt
|
||||||
|
})),
|
||||||
|
// @ts-expect-error col select is raw and not strongly typed
|
||||||
|
totalCount: Number(hosts?.[0]?.total_count ?? 0)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.SshHostGroupMembership}: FindAllSshHostsInGroup` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
findSshHostGroupsWithLoginMappings,
|
||||||
|
findSshHostGroupByIdWithLoginMappings,
|
||||||
|
findAllSshHostsInGroup,
|
||||||
|
...sshHostGroupOrm
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshHostGroupMembershipDALFactory = ReturnType<typeof sshHostGroupMembershipDALFactory>;
|
||||||
|
|
||||||
|
export const sshHostGroupMembershipDALFactory = (db: TDbClient) => {
|
||||||
|
const sshHostGroupMembershipOrm = ormify(db, TableName.SshHostGroupMembership);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...sshHostGroupMembershipOrm
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { SshHostGroupsSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const sanitizedSshHostGroup = SshHostGroupsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
projectId: true,
|
||||||
|
name: true
|
||||||
|
});
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
||||||
|
import { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
|
||||||
|
import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||||
|
import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal";
|
||||||
|
import { TSshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-membership-dal";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
|
import { TGroupDALFactory } from "../group/group-dal";
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import { createSshLoginMappings } from "../ssh-host/ssh-host-fns";
|
||||||
|
import {
|
||||||
|
TAddHostToSshHostGroupDTO,
|
||||||
|
TCreateSshHostGroupDTO,
|
||||||
|
TDeleteSshHostGroupDTO,
|
||||||
|
TGetSshHostGroupDTO,
|
||||||
|
TListSshHostGroupHostsDTO,
|
||||||
|
TRemoveHostFromSshHostGroupDTO,
|
||||||
|
TUpdateSshHostGroupDTO
|
||||||
|
} from "./ssh-host-group-types";
|
||||||
|
|
||||||
|
type TSshHostGroupServiceFactoryDep = {
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findById" | "find">;
|
||||||
|
sshHostDAL: Pick<TSshHostDALFactory, "findSshHostByIdWithLoginMappings">;
|
||||||
|
sshHostGroupDAL: Pick<
|
||||||
|
TSshHostGroupDALFactory,
|
||||||
|
| "create"
|
||||||
|
| "updateById"
|
||||||
|
| "findById"
|
||||||
|
| "deleteById"
|
||||||
|
| "transaction"
|
||||||
|
| "findSshHostGroupByIdWithLoginMappings"
|
||||||
|
| "findAllSshHostsInGroup"
|
||||||
|
| "findOne"
|
||||||
|
| "find"
|
||||||
|
>;
|
||||||
|
sshHostGroupMembershipDAL: Pick<TSshHostGroupMembershipDALFactory, "create" | "deleteById" | "findOne">;
|
||||||
|
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction" | "delete">;
|
||||||
|
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
|
||||||
|
userDAL: Pick<TUserDALFactory, "find">;
|
||||||
|
permissionService: Pick<
|
||||||
|
TPermissionServiceFactory,
|
||||||
|
"getProjectPermission" | "getUserProjectPermission" | "checkGroupProjectPermission"
|
||||||
|
>;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
groupDAL: Pick<TGroupDALFactory, "findGroupsByProjectId">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSshHostGroupServiceFactory = ReturnType<typeof sshHostGroupServiceFactory>;
|
||||||
|
|
||||||
|
export const sshHostGroupServiceFactory = ({
|
||||||
|
projectDAL,
|
||||||
|
sshHostDAL,
|
||||||
|
sshHostGroupDAL,
|
||||||
|
sshHostGroupMembershipDAL,
|
||||||
|
sshHostLoginUserDAL,
|
||||||
|
sshHostLoginUserMappingDAL,
|
||||||
|
userDAL,
|
||||||
|
permissionService,
|
||||||
|
licenseService,
|
||||||
|
groupDAL
|
||||||
|
}: TSshHostGroupServiceFactoryDep) => {
|
||||||
|
const createSshHostGroup = async ({
|
||||||
|
projectId,
|
||||||
|
name,
|
||||||
|
loginMappings,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TCreateSshHostGroupDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.SshHostGroups);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.sshHostGroups)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to create SSH host group due to plan restriction. Upgrade plan to create group."
|
||||||
|
});
|
||||||
|
|
||||||
|
const newSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => {
|
||||||
|
// (dangtony98): room to optimize check to ensure that
|
||||||
|
// the SSH host group name is unique across the whole org
|
||||||
|
const project = await projectDAL.findById(projectId, tx);
|
||||||
|
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
|
||||||
|
const projects = await projectDAL.find(
|
||||||
|
{
|
||||||
|
orgId: project.orgId
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
|
||||||
|
const existingSshHostGroup = await sshHostGroupDAL.find(
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
$in: {
|
||||||
|
projectId: projects.map((p) => p.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingSshHostGroup.length) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `SSH host group with name '${name}' already exists in the organization`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sshHostGroup = await sshHostGroupDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
name
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await createSshLoginMappings({
|
||||||
|
sshHostGroupId: sshHostGroup.id,
|
||||||
|
loginMappings,
|
||||||
|
sshHostLoginUserDAL,
|
||||||
|
sshHostLoginUserMappingDAL,
|
||||||
|
groupDAL,
|
||||||
|
userDAL,
|
||||||
|
permissionService,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
|
||||||
|
const newSshHostGroupWithLoginMappings = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(
|
||||||
|
sshHostGroup.id,
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
if (!newSshHostGroupWithLoginMappings) {
|
||||||
|
throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroup.id}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSshHostGroupWithLoginMappings;
|
||||||
|
});
|
||||||
|
|
||||||
|
return newSshHostGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSshHostGroup = async ({
|
||||||
|
sshHostGroupId,
|
||||||
|
name,
|
||||||
|
loginMappings,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TUpdateSshHostGroupDTO) => {
|
||||||
|
const sshHostGroup = await sshHostGroupDAL.findById(sshHostGroupId);
|
||||||
|
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: sshHostGroup.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SshHostGroups);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.sshHostGroups)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to update SSH host group due to plan restriction. Upgrade plan to update group."
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => {
|
||||||
|
await sshHostGroupDAL.updateById(
|
||||||
|
sshHostGroupId,
|
||||||
|
{
|
||||||
|
name
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
if (loginMappings) {
|
||||||
|
await sshHostLoginUserDAL.delete({ sshHostGroupId: sshHostGroup.id }, tx);
|
||||||
|
if (loginMappings.length) {
|
||||||
|
await createSshLoginMappings({
|
||||||
|
sshHostGroupId: sshHostGroup.id,
|
||||||
|
loginMappings,
|
||||||
|
sshHostLoginUserDAL,
|
||||||
|
sshHostLoginUserMappingDAL,
|
||||||
|
groupDAL,
|
||||||
|
userDAL,
|
||||||
|
permissionService,
|
||||||
|
projectId: sshHostGroup.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSshHostGroupWithLoginMappings = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(
|
||||||
|
sshHostGroup.id,
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
if (!updatedSshHostGroupWithLoginMappings) {
|
||||||
|
throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroup.id}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedSshHostGroupWithLoginMappings;
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedSshHostGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSshHostGroup = async ({
|
||||||
|
sshHostGroupId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TGetSshHostGroupDTO) => {
|
||||||
|
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||||
|
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: sshHostGroup.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHostGroups);
|
||||||
|
|
||||||
|
return sshHostGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSshHostGroup = async ({
|
||||||
|
sshHostGroupId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TDeleteSshHostGroupDTO) => {
|
||||||
|
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||||
|
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: sshHostGroup.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.SshHostGroups);
|
||||||
|
|
||||||
|
await sshHostGroupDAL.deleteById(sshHostGroupId);
|
||||||
|
|
||||||
|
return sshHostGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
const listSshHostGroupHosts = async ({
|
||||||
|
sshHostGroupId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
filter
|
||||||
|
}: TListSshHostGroupHostsDTO) => {
|
||||||
|
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||||
|
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: sshHostGroup.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHostGroups);
|
||||||
|
|
||||||
|
const { hosts, totalCount } = await sshHostGroupDAL.findAllSshHostsInGroup({ sshHostGroupId, filter });
|
||||||
|
return { sshHostGroup, hosts, totalCount };
|
||||||
|
};
|
||||||
|
|
||||||
|
const addHostToSshHostGroup = async ({
|
||||||
|
sshHostGroupId,
|
||||||
|
hostId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TAddHostToSshHostGroupDTO) => {
|
||||||
|
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||||
|
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||||
|
|
||||||
|
const sshHost = await sshHostDAL.findSshHostByIdWithLoginMappings(hostId);
|
||||||
|
if (!sshHost) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH host with ID ${hostId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sshHostGroup.projectId !== sshHost.projectId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `SSH host with ID ${hostId} not found in project ${sshHostGroup.projectId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: sshHostGroup.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SshHostGroups);
|
||||||
|
|
||||||
|
await sshHostGroupMembershipDAL.create({ sshHostGroupId, sshHostId: hostId });
|
||||||
|
|
||||||
|
return { sshHostGroup, sshHost };
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeHostFromSshHostGroup = async ({
|
||||||
|
sshHostGroupId,
|
||||||
|
hostId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TRemoveHostFromSshHostGroupDTO) => {
|
||||||
|
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||||
|
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||||
|
|
||||||
|
const sshHost = await sshHostDAL.findSshHostByIdWithLoginMappings(hostId);
|
||||||
|
if (!sshHost) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH host with ID ${hostId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sshHostGroup.projectId !== sshHost.projectId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `SSH host with ID ${hostId} not found in project ${sshHostGroup.projectId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: sshHostGroup.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SshHostGroups);
|
||||||
|
|
||||||
|
const sshHostGroupMembership = await sshHostGroupMembershipDAL.findOne({
|
||||||
|
sshHostGroupId,
|
||||||
|
sshHostId: hostId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sshHostGroupMembership) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH host with ID ${hostId} not found in SSH host group with ID ${sshHostGroupId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await sshHostGroupMembershipDAL.deleteById(sshHostGroupMembership.id);
|
||||||
|
|
||||||
|
return { sshHostGroup, sshHost };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createSshHostGroup,
|
||||||
|
getSshHostGroup,
|
||||||
|
deleteSshHostGroup,
|
||||||
|
updateSshHostGroup,
|
||||||
|
listSshHostGroupHosts,
|
||||||
|
addHostToSshHostGroup,
|
||||||
|
removeHostFromSshHostGroup
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { TLoginMapping } from "@app/ee/services/ssh-host/ssh-host-types";
|
||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TCreateSshHostGroupDTO = {
|
||||||
|
name: string;
|
||||||
|
loginMappings: TLoginMapping[];
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateSshHostGroupDTO = {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
name?: string;
|
||||||
|
loginMappings?: TLoginMapping[];
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetSshHostGroupDTO = {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteSshHostGroupDTO = {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TListSshHostGroupHostsDTO = {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
filter?: EHostGroupMembershipFilter;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TAddHostToSshHostGroupDTO = {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
hostId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TRemoveHostFromSshHostGroupDTO = {
|
||||||
|
sshHostGroupId: string;
|
||||||
|
hostId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export enum EHostGroupMembershipFilter {
|
||||||
|
GROUP_MEMBERS = "group-members",
|
||||||
|
NON_GROUP_MEMBERS = "non-group-members"
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import { DatabaseError } from "@app/lib/errors";
|
|||||||
import { groupBy, unique } from "@app/lib/fn";
|
import { groupBy, unique } from "@app/lib/fn";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
import { LoginMappingSource } from "./ssh-host-types";
|
||||||
|
|
||||||
export type TSshHostDALFactory = ReturnType<typeof sshHostDALFactory>;
|
export type TSshHostDALFactory = ReturnType<typeof sshHostDALFactory>;
|
||||||
|
|
||||||
export const sshHostDALFactory = (db: TDbClient) => {
|
export const sshHostDALFactory = (db: TDbClient) => {
|
||||||
@@ -13,13 +15,16 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findUserAccessibleSshHosts = async (projectIds: string[], userId: string, tx?: Knex) => {
|
const findUserAccessibleSshHosts = async (projectIds: string[], userId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const user = await (tx || db.replicaNode())(TableName.Users).where({ id: userId }).select("username").first();
|
const knex = tx || db.replicaNode();
|
||||||
|
|
||||||
|
const user = await knex(TableName.Users).where({ id: userId }).select("username").first();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new DatabaseError({ name: `${TableName.Users}: UserNotFound`, error: new Error("User not found") });
|
throw new DatabaseError({ name: `${TableName.Users}: UserNotFound`, error: new Error("User not found") });
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = await (tx || db.replicaNode())(TableName.SshHost)
|
// get hosts where user has direct login mappings
|
||||||
|
const directHostRows = await knex(TableName.SshHost)
|
||||||
.leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`)
|
.leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.SshHostLoginUserMapping,
|
TableName.SshHostLoginUserMapping,
|
||||||
@@ -46,26 +51,79 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||||
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||||
db.ref("username").withSchema(TableName.Users),
|
|
||||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
|
||||||
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||||
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||||
)
|
);
|
||||||
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
|
|
||||||
|
|
||||||
const grouped = groupBy(rows, (r) => r.sshHostId);
|
// get hosts where user has login mappings via host groups
|
||||||
return Object.values(grouped).map((hostRows) => {
|
const groupHostRows = await knex(TableName.SshHostGroupMembership)
|
||||||
|
.join(
|
||||||
|
TableName.SshHostLoginUser,
|
||||||
|
`${TableName.SshHostGroupMembership}.sshHostGroupId`,
|
||||||
|
`${TableName.SshHostLoginUser}.sshHostGroupId`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SshHostLoginUserMapping,
|
||||||
|
`${TableName.SshHostLoginUser}.id`,
|
||||||
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
|
)
|
||||||
|
.join(TableName.SshHost, `${TableName.SshHostGroupMembership}.sshHostId`, `${TableName.SshHost}.id`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.UserGroupMembership,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`,
|
||||||
|
`${TableName.SshHostLoginUserMapping}.groupId`
|
||||||
|
)
|
||||||
|
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
|
||||||
|
.andWhere((bd) => {
|
||||||
|
void bd
|
||||||
|
.where(`${TableName.SshHostLoginUserMapping}.userId`, userId)
|
||||||
|
.orWhere(`${TableName.UserGroupMembership}.userId`, userId);
|
||||||
|
})
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||||
|
db.ref("projectId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostname").withSchema(TableName.SshHost),
|
||||||
|
db.ref("alias").withSchema(TableName.SshHost),
|
||||||
|
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||||
|
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||||
|
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||||
|
);
|
||||||
|
|
||||||
|
const directHostRowsWithSource = directHostRows.map((row) => ({
|
||||||
|
...row,
|
||||||
|
source: LoginMappingSource.HOST
|
||||||
|
}));
|
||||||
|
|
||||||
|
const groupHostRowsWithSource = groupHostRows.map((row) => ({
|
||||||
|
...row,
|
||||||
|
source: LoginMappingSource.HOST_GROUP
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mergedRows = [...directHostRowsWithSource, ...groupHostRowsWithSource];
|
||||||
|
|
||||||
|
const hostsGrouped = groupBy(mergedRows, (r) => r.sshHostId);
|
||||||
|
|
||||||
|
return Object.values(hostsGrouped).map((hostRows) => {
|
||||||
const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } =
|
const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } =
|
||||||
hostRows[0];
|
hostRows[0];
|
||||||
|
|
||||||
const loginMappingGrouped = groupBy(hostRows, (r) => r.loginUser);
|
const loginMappingGrouped = groupBy(hostRows, (r) => r.loginUser);
|
||||||
|
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, mappings]) => {
|
||||||
|
// Prefer HOST source over HOST_GROUP
|
||||||
|
const preferredMapping =
|
||||||
|
mappings.find((m) => m.source === LoginMappingSource.HOST) ||
|
||||||
|
mappings.find((m) => m.source === LoginMappingSource.HOST_GROUP);
|
||||||
|
|
||||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser]) => ({
|
return {
|
||||||
loginUser,
|
loginUser,
|
||||||
allowedPrincipals: {
|
allowedPrincipals: {
|
||||||
usernames: [user.username]
|
usernames: [user.username]
|
||||||
}
|
},
|
||||||
}));
|
source: preferredMapping!.source
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: sshHostId,
|
id: sshHostId,
|
||||||
@@ -112,21 +170,61 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
)
|
)
|
||||||
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
|
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
|
||||||
|
|
||||||
|
// process login mappings inherited from groups that hosts are part of
|
||||||
|
const hostIds = unique(rows.map((r) => r.sshHostId)).filter(Boolean);
|
||||||
|
const groupRows = await (tx || db.replicaNode())(TableName.SshHostGroupMembership)
|
||||||
|
.join(
|
||||||
|
TableName.SshHostLoginUser,
|
||||||
|
`${TableName.SshHostGroupMembership}.sshHostGroupId`,
|
||||||
|
`${TableName.SshHostLoginUser}.sshHostGroupId`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SshHostLoginUserMapping,
|
||||||
|
`${TableName.SshHostLoginUser}.id`,
|
||||||
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||||
|
.select(
|
||||||
|
db.ref("sshHostId").withSchema(TableName.SshHostGroupMembership),
|
||||||
|
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||||
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||||
|
)
|
||||||
|
.whereIn(`${TableName.SshHostGroupMembership}.sshHostId`, hostIds);
|
||||||
|
|
||||||
|
const groupedGroupMappings = groupBy(groupRows, (r) => r.sshHostId);
|
||||||
|
|
||||||
const hostsGrouped = groupBy(rows, (r) => r.sshHostId);
|
const hostsGrouped = groupBy(rows, (r) => r.sshHostId);
|
||||||
return Object.values(hostsGrouped).map((hostRows) => {
|
return Object.values(hostsGrouped).map((hostRows) => {
|
||||||
const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0];
|
const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0];
|
||||||
|
|
||||||
|
// direct login mappings
|
||||||
const loginMappingGrouped = groupBy(
|
const loginMappingGrouped = groupBy(
|
||||||
hostRows.filter((r) => r.loginUser),
|
hostRows.filter((r) => r.loginUser),
|
||||||
(r) => r.loginUser
|
(r) => r.loginUser
|
||||||
);
|
);
|
||||||
|
|
||||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
const directMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||||
loginUser,
|
loginUser,
|
||||||
allowedPrincipals: {
|
allowedPrincipals: {
|
||||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||||
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||||
}
|
},
|
||||||
|
source: LoginMappingSource.HOST
|
||||||
|
}));
|
||||||
|
|
||||||
|
// group-inherited login mappings
|
||||||
|
const inheritedGroupRows = groupedGroupMappings[sshHostId] || [];
|
||||||
|
const inheritedGrouped = groupBy(inheritedGroupRows, (r) => r.loginUser);
|
||||||
|
|
||||||
|
const groupMappings = Object.entries(inheritedGrouped).map(([loginUser, entries]) => ({
|
||||||
|
loginUser,
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||||
|
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||||
|
},
|
||||||
|
source: LoginMappingSource.HOST_GROUP
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -136,7 +234,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
projectId,
|
projectId,
|
||||||
userCertTtl,
|
userCertTtl,
|
||||||
hostCertTtl,
|
hostCertTtl,
|
||||||
loginMappings,
|
loginMappings: [...directMappings, ...groupMappings],
|
||||||
userSshCaId,
|
userSshCaId,
|
||||||
hostSshCaId
|
hostSshCaId
|
||||||
};
|
};
|
||||||
@@ -177,17 +275,54 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const { sshHostId: id, projectId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0];
|
const { sshHostId: id, projectId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0];
|
||||||
|
|
||||||
const loginMappingGrouped = groupBy(
|
// direct login mappings
|
||||||
|
const directGrouped = groupBy(
|
||||||
rows.filter((r) => r.loginUser),
|
rows.filter((r) => r.loginUser),
|
||||||
(r) => r.loginUser
|
(r) => r.loginUser
|
||||||
);
|
);
|
||||||
|
|
||||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
const directMappings = Object.entries(directGrouped).map(([loginUser, entries]) => ({
|
||||||
loginUser,
|
loginUser,
|
||||||
allowedPrincipals: {
|
allowedPrincipals: {
|
||||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||||
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||||
}
|
},
|
||||||
|
source: LoginMappingSource.HOST
|
||||||
|
}));
|
||||||
|
|
||||||
|
// group login mappings
|
||||||
|
const groupRows = await (tx || db.replicaNode())(TableName.SshHostGroupMembership)
|
||||||
|
.join(
|
||||||
|
TableName.SshHostLoginUser,
|
||||||
|
`${TableName.SshHostGroupMembership}.sshHostGroupId`,
|
||||||
|
`${TableName.SshHostLoginUser}.sshHostGroupId`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SshHostLoginUserMapping,
|
||||||
|
`${TableName.SshHostLoginUser}.id`,
|
||||||
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||||
|
.where(`${TableName.SshHostGroupMembership}.sshHostId`, sshHostId)
|
||||||
|
.select(
|
||||||
|
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||||
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupGrouped = groupBy(
|
||||||
|
groupRows.filter((r) => r.loginUser),
|
||||||
|
(r) => r.loginUser
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupMappings = Object.entries(groupGrouped).map(([loginUser, entries]) => ({
|
||||||
|
loginUser,
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||||
|
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||||
|
},
|
||||||
|
source: LoginMappingSource.HOST_GROUP
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -197,7 +332,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
alias,
|
alias,
|
||||||
userCertTtl,
|
userCertTtl,
|
||||||
hostCertTtl,
|
hostCertTtl,
|
||||||
loginMappings,
|
loginMappings: [...directMappings, ...groupMappings],
|
||||||
userSshCaId,
|
userSshCaId,
|
||||||
hostSshCaId
|
hostSshCaId
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||||
|
import { TCreateSshLoginMappingsDTO } from "./ssh-host-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create SSH login mappings for a given SSH host
|
||||||
|
* or SSH host group.
|
||||||
|
*/
|
||||||
|
export const createSshLoginMappings = async ({
|
||||||
|
sshHostId,
|
||||||
|
sshHostGroupId,
|
||||||
|
loginMappings,
|
||||||
|
sshHostLoginUserDAL,
|
||||||
|
sshHostLoginUserMappingDAL,
|
||||||
|
groupDAL,
|
||||||
|
userDAL,
|
||||||
|
permissionService,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
tx: outerTx
|
||||||
|
}: TCreateSshLoginMappingsDTO) => {
|
||||||
|
const processCreation = async (tx: Knex) => {
|
||||||
|
// (dangtony98): room to optimize
|
||||||
|
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
||||||
|
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
||||||
|
// (dangtony98): should either pass in sshHostId or sshHostGroupId but not both
|
||||||
|
{
|
||||||
|
sshHostId,
|
||||||
|
sshHostGroupId,
|
||||||
|
loginUser
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allowedPrincipals.usernames && allowedPrincipals.usernames.length > 0) {
|
||||||
|
const users = await userDAL.find(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
username: allowedPrincipals.usernames
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
|
||||||
|
const foundUsernames = new Set(users.map((u) => u.username));
|
||||||
|
|
||||||
|
for (const uname of allowedPrincipals.usernames) {
|
||||||
|
if (!foundUsernames.has(uname)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid username: ${uname}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const user of users) {
|
||||||
|
// check that each user has access to the SSH project
|
||||||
|
await permissionService.getUserProjectPermission({
|
||||||
|
userId: user.id,
|
||||||
|
projectId,
|
||||||
|
authMethod: actorAuthMethod,
|
||||||
|
userOrgId: actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await sshHostLoginUserMappingDAL.insertMany(
|
||||||
|
users.map((user) => ({
|
||||||
|
sshHostLoginUserId: sshHostLoginUser.id,
|
||||||
|
userId: user.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowedPrincipals.groups && allowedPrincipals.groups.length > 0) {
|
||||||
|
const projectGroups = await groupDAL.findGroupsByProjectId(projectId);
|
||||||
|
const groups = projectGroups.filter((g) => allowedPrincipals.groups?.includes(g.slug));
|
||||||
|
|
||||||
|
if (groups.length !== allowedPrincipals.groups?.length) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid group slugs: ${allowedPrincipals.groups
|
||||||
|
.filter((g) => !projectGroups.some((pg) => pg.slug === g))
|
||||||
|
.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const group of groups) {
|
||||||
|
// check that each group has access to the SSH project and have read access to hosts
|
||||||
|
const hasPermission = await permissionService.checkGroupProjectPermission({
|
||||||
|
groupId: group.id,
|
||||||
|
projectId,
|
||||||
|
checkPermissions: [ProjectPermissionSshHostActions.Read, ProjectPermissionSub.SshHosts]
|
||||||
|
});
|
||||||
|
if (!hasPermission) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Group ${group.slug} does not have access to the SSH project`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await sshHostLoginUserMappingDAL.insertMany(
|
||||||
|
groups.map((group) => ({
|
||||||
|
sshHostLoginUserId: sshHostLoginUser.id,
|
||||||
|
groupId: group.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (outerTx) {
|
||||||
|
return processCreation(outerTx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sshHostLoginUserDAL.transaction(processCreation);
|
||||||
|
};
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
getSshPublicKey
|
getSshPublicKey
|
||||||
} from "../ssh/ssh-certificate-authority-fns";
|
} from "../ssh/ssh-certificate-authority-fns";
|
||||||
import { SshCertType } from "../ssh/ssh-certificate-authority-types";
|
import { SshCertType } from "../ssh/ssh-certificate-authority-types";
|
||||||
|
import { createSshLoginMappings } from "./ssh-host-fns";
|
||||||
import {
|
import {
|
||||||
TCreateSshHostDTO,
|
TCreateSshHostDTO,
|
||||||
TDeleteSshHostDTO,
|
TDeleteSshHostDTO,
|
||||||
@@ -211,91 +212,19 @@ export const sshHostServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
// (dangtony98): room to optimize
|
await createSshLoginMappings({
|
||||||
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
sshHostId: host.id,
|
||||||
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
loginMappings,
|
||||||
{
|
sshHostLoginUserDAL,
|
||||||
sshHostId: host.id,
|
sshHostLoginUserMappingDAL,
|
||||||
loginUser
|
groupDAL,
|
||||||
},
|
userDAL,
|
||||||
tx
|
permissionService,
|
||||||
);
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
if (allowedPrincipals.usernames && allowedPrincipals.usernames.length > 0) {
|
actorOrgId,
|
||||||
const users = await userDAL.find(
|
tx
|
||||||
{
|
});
|
||||||
$in: {
|
|
||||||
username: allowedPrincipals.usernames
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ tx }
|
|
||||||
);
|
|
||||||
|
|
||||||
const foundUsernames = new Set(users.map((u) => u.username));
|
|
||||||
|
|
||||||
for (const uname of allowedPrincipals.usernames) {
|
|
||||||
if (!foundUsernames.has(uname)) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Invalid username: ${uname}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for await (const user of users) {
|
|
||||||
// check that each user has access to the SSH project
|
|
||||||
await permissionService.getUserProjectPermission({
|
|
||||||
userId: user.id,
|
|
||||||
projectId,
|
|
||||||
authMethod: actorAuthMethod,
|
|
||||||
userOrgId: actorOrgId,
|
|
||||||
actionProjectType: ActionProjectType.SSH
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await sshHostLoginUserMappingDAL.insertMany(
|
|
||||||
users.map((user) => ({
|
|
||||||
sshHostLoginUserId: sshHostLoginUser.id,
|
|
||||||
userId: user.id
|
|
||||||
})),
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowedPrincipals.groups && allowedPrincipals.groups.length > 0) {
|
|
||||||
const projectGroups = await groupDAL.findGroupsByProjectId(projectId);
|
|
||||||
const groups = projectGroups.filter((g) => allowedPrincipals.groups?.includes(g.slug));
|
|
||||||
|
|
||||||
if (groups.length !== allowedPrincipals.groups?.length) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Invalid group slugs: ${allowedPrincipals.groups
|
|
||||||
.filter((g) => !projectGroups.some((pg) => pg.slug === g))
|
|
||||||
.join(", ")}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for await (const group of groups) {
|
|
||||||
// check that each group has access to the SSH project and have read access to hosts
|
|
||||||
const hasPermission = await permissionService.checkGroupProjectPermission({
|
|
||||||
groupId: group.id,
|
|
||||||
projectId,
|
|
||||||
checkPermissions: [ProjectPermissionSshHostActions.Read, ProjectPermissionSub.SshHosts]
|
|
||||||
});
|
|
||||||
if (!hasPermission) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Group ${group.slug} does not have access to the SSH project`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await sshHostLoginUserMappingDAL.insertMany(
|
|
||||||
groups.map((group) => ({
|
|
||||||
sshHostLoginUserId: sshHostLoginUser.id,
|
|
||||||
groupId: group.id
|
|
||||||
})),
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newSshHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(host.id, tx);
|
const newSshHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(host.id, tx);
|
||||||
if (!newSshHostWithLoginMappings) {
|
if (!newSshHostWithLoginMappings) {
|
||||||
@@ -354,90 +283,19 @@ export const sshHostServiceFactory = ({
|
|||||||
if (loginMappings) {
|
if (loginMappings) {
|
||||||
await sshHostLoginUserDAL.delete({ sshHostId: host.id }, tx);
|
await sshHostLoginUserDAL.delete({ sshHostId: host.id }, tx);
|
||||||
if (loginMappings.length) {
|
if (loginMappings.length) {
|
||||||
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
await createSshLoginMappings({
|
||||||
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
sshHostId: host.id,
|
||||||
{
|
loginMappings,
|
||||||
sshHostId: host.id,
|
sshHostLoginUserDAL,
|
||||||
loginUser
|
sshHostLoginUserMappingDAL,
|
||||||
},
|
groupDAL,
|
||||||
tx
|
userDAL,
|
||||||
);
|
permissionService,
|
||||||
|
projectId: host.projectId,
|
||||||
if (allowedPrincipals.usernames && allowedPrincipals.usernames.length > 0) {
|
actorAuthMethod,
|
||||||
const users = await userDAL.find(
|
actorOrgId,
|
||||||
{
|
tx
|
||||||
$in: {
|
});
|
||||||
username: allowedPrincipals.usernames
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ tx }
|
|
||||||
);
|
|
||||||
|
|
||||||
const foundUsernames = new Set(users.map((u) => u.username));
|
|
||||||
|
|
||||||
for (const uname of allowedPrincipals.usernames) {
|
|
||||||
if (!foundUsernames.has(uname)) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Invalid username: ${uname}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for await (const user of users) {
|
|
||||||
await permissionService.getUserProjectPermission({
|
|
||||||
userId: user.id,
|
|
||||||
projectId: host.projectId,
|
|
||||||
authMethod: actorAuthMethod,
|
|
||||||
userOrgId: actorOrgId,
|
|
||||||
actionProjectType: ActionProjectType.SSH
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await sshHostLoginUserMappingDAL.insertMany(
|
|
||||||
users.map((user) => ({
|
|
||||||
sshHostLoginUserId: sshHostLoginUser.id,
|
|
||||||
userId: user.id
|
|
||||||
})),
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowedPrincipals.groups && allowedPrincipals.groups.length > 0) {
|
|
||||||
const groups = await groupDAL.findGroupsByProjectId(host.projectId);
|
|
||||||
|
|
||||||
const foundGroupSlugs = new Set(groups.map((g) => g.slug));
|
|
||||||
|
|
||||||
for (const slug of allowedPrincipals.groups) {
|
|
||||||
if (!foundGroupSlugs.has(slug)) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Invalid group slug: ${slug}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for await (const group of groups) {
|
|
||||||
// check that each group has access to the SSH project and have read access to hosts
|
|
||||||
const hasPermission = await permissionService.checkGroupProjectPermission({
|
|
||||||
groupId: group.id,
|
|
||||||
projectId: host.projectId,
|
|
||||||
checkPermissions: [ProjectPermissionSshHostActions.Read, ProjectPermissionSub.SshHosts]
|
|
||||||
});
|
|
||||||
if (!hasPermission) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Group ${group.slug} does not have access to the SSH project`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await sshHostLoginUserMappingDAL.insertMany(
|
|
||||||
groups.map((group) => ({
|
|
||||||
sshHostLoginUserId: sshHostLoginUser.id,
|
|
||||||
groupId: group.id
|
|
||||||
})),
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,8 +404,8 @@ export const sshHostServiceFactory = ({
|
|||||||
const mapping = host.loginMappings.find(
|
const mapping = host.loginMappings.find(
|
||||||
(m) =>
|
(m) =>
|
||||||
m.loginUser === loginUser &&
|
m.loginUser === loginUser &&
|
||||||
(m.allowedPrincipals.usernames.some((allowed) => internalPrincipals.includes(allowed)) ||
|
(m.allowedPrincipals.usernames?.some((allowed) => internalPrincipals.includes(allowed)) ||
|
||||||
m.allowedPrincipals.groups.some((allowed) => userGroupSlugs.includes(allowed)))
|
m.allowedPrincipals.groups?.some((allowed) => userGroupSlugs.includes(allowed)))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mapping) {
|
if (!mapping) {
|
||||||
|
|||||||
@@ -1,19 +1,35 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
|
||||||
|
import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||||
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
|
import { TGroupDALFactory } from "../group/group-dal";
|
||||||
|
|
||||||
export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
|
export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TLoginMapping = {
|
||||||
|
loginUser: string;
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames?: string[];
|
||||||
|
groups?: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum LoginMappingSource {
|
||||||
|
HOST = "host",
|
||||||
|
HOST_GROUP = "hostGroup"
|
||||||
|
}
|
||||||
|
|
||||||
export type TCreateSshHostDTO = {
|
export type TCreateSshHostDTO = {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
alias?: string;
|
alias?: string;
|
||||||
userCertTtl: string;
|
userCertTtl: string;
|
||||||
hostCertTtl: string;
|
hostCertTtl: string;
|
||||||
loginMappings: {
|
loginMappings: TLoginMapping[];
|
||||||
loginUser: string;
|
|
||||||
allowedPrincipals: {
|
|
||||||
usernames?: string[];
|
|
||||||
groups?: string[];
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
userSshCaId?: string;
|
userSshCaId?: string;
|
||||||
hostSshCaId?: string;
|
hostSshCaId?: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
@@ -24,13 +40,7 @@ export type TUpdateSshHostDTO = {
|
|||||||
alias?: string;
|
alias?: string;
|
||||||
userCertTtl?: string;
|
userCertTtl?: string;
|
||||||
hostCertTtl?: string;
|
hostCertTtl?: string;
|
||||||
loginMappings?: {
|
loginMappings?: TLoginMapping[];
|
||||||
loginUser: string;
|
|
||||||
allowedPrincipals: {
|
|
||||||
usernames?: string[];
|
|
||||||
groups?: string[];
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TGetSshHostDTO = {
|
export type TGetSshHostDTO = {
|
||||||
@@ -50,3 +60,20 @@ export type TIssueSshHostHostCertDTO = {
|
|||||||
sshHostId: string;
|
sshHostId: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
type BaseCreateSshLoginMappingsDTO = {
|
||||||
|
loginMappings: TLoginMapping[];
|
||||||
|
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction">;
|
||||||
|
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
|
||||||
|
userDAL: Pick<TUserDALFactory, "find">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getUserProjectPermission" | "checkGroupProjectPermission">;
|
||||||
|
groupDAL: Pick<TGroupDALFactory, "findGroupsByProjectId">;
|
||||||
|
projectId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
actorOrgId: string;
|
||||||
|
tx?: Knex;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateSshLoginMappingsDTO =
|
||||||
|
| (BaseCreateSshLoginMappingsDTO & { sshHostId: string; sshHostGroupId?: undefined })
|
||||||
|
| (BaseCreateSshLoginMappingsDTO & { sshHostGroupId: string; sshHostId?: undefined });
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
|
|
||||||
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
||||||
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
||||||
? requestedKeyId ?? `${actor}-${actorId}`
|
? (requestedKeyId ?? `${actor}-${actorId}`)
|
||||||
: `${actor}-${actorId}`;
|
: `${actor}-${actorId}`;
|
||||||
|
|
||||||
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
||||||
@@ -404,7 +404,7 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
|
|
||||||
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
||||||
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
||||||
? requestedKeyId ?? `${actor}-${actorId}`
|
? (requestedKeyId ?? `${actor}-${actorId}`)
|
||||||
: `${actor}-${actorId}`;
|
: `${actor}-${actorId}`;
|
||||||
|
|
||||||
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ export enum ApiDocsTags {
|
|||||||
SshCertificates = "SSH Certificates",
|
SshCertificates = "SSH Certificates",
|
||||||
SshCertificateAuthorities = "SSH Certificate Authorities",
|
SshCertificateAuthorities = "SSH Certificate Authorities",
|
||||||
SshCertificateTemplates = "SSH Certificate Templates",
|
SshCertificateTemplates = "SSH Certificate Templates",
|
||||||
|
SshHosts = "SSH Hosts",
|
||||||
|
SshHostGroups = "SSH Host Groups",
|
||||||
KmsKeys = "KMS Keys",
|
KmsKeys = "KMS Keys",
|
||||||
KmsEncryption = "KMS Encryption",
|
KmsEncryption = "KMS Encryption",
|
||||||
KmsSigning = "KMS Signing"
|
KmsSigning = "KMS Signing"
|
||||||
@@ -568,6 +570,9 @@ export const PROJECTS = {
|
|||||||
LIST_SSH_HOSTS: {
|
LIST_SSH_HOSTS: {
|
||||||
projectId: "The ID of the project to list SSH hosts for."
|
projectId: "The ID of the project to list SSH hosts for."
|
||||||
},
|
},
|
||||||
|
LIST_SSH_HOST_GROUPS: {
|
||||||
|
projectId: "The ID of the project to list SSH host groups for."
|
||||||
|
},
|
||||||
LIST_SSH_CERTIFICATES: {
|
LIST_SSH_CERTIFICATES: {
|
||||||
projectId: "The ID of the project to list SSH certificates for.",
|
projectId: "The ID of the project to list SSH certificates for.",
|
||||||
offset: "The offset to start from. If you enter 10, it will start from the 10th SSH certificate.",
|
offset: "The offset to start from. If you enter 10, it will start from the 10th SSH certificate.",
|
||||||
@@ -1382,6 +1387,40 @@ export const SSH_CERTIFICATE_TEMPLATES = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SSH_HOST_GROUPS = {
|
||||||
|
GET: {
|
||||||
|
sshHostGroupId: "The ID of the SSH host group to get.",
|
||||||
|
filter: "The filter to apply to the SSH hosts in the SSH host group."
|
||||||
|
},
|
||||||
|
CREATE: {
|
||||||
|
projectId: "The ID of the project to create the SSH host group in.",
|
||||||
|
name: "The name of the SSH host group.",
|
||||||
|
loginMappings:
|
||||||
|
"A list of default login mappings to include on each host in the SSH host group. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project."
|
||||||
|
},
|
||||||
|
UPDATE: {
|
||||||
|
sshHostGroupId: "The ID of the SSH host group to update.",
|
||||||
|
name: "The name of the SSH host group to update to.",
|
||||||
|
loginMappings:
|
||||||
|
"A list of default login mappings to include on each host in the SSH host group. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project."
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
sshHostGroupId: "The ID of the SSH host group to delete."
|
||||||
|
},
|
||||||
|
LIST_HOSTS: {
|
||||||
|
offset: "The offset to start from. If you enter 10, it will start from the 10th host",
|
||||||
|
limit: "The number of hosts to return."
|
||||||
|
},
|
||||||
|
ADD_HOST: {
|
||||||
|
sshHostGroupId: "The ID of the SSH host group to add the host to.",
|
||||||
|
hostId: "The ID of the SSH host to add to the SSH host group."
|
||||||
|
},
|
||||||
|
DELETE_HOST: {
|
||||||
|
sshHostGroupId: "The ID of the SSH host group to delete the host from.",
|
||||||
|
hostId: "The ID of the SSH host to delete from the SSH host group."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const SSH_HOSTS = {
|
export const SSH_HOSTS = {
|
||||||
GET: {
|
GET: {
|
||||||
sshHostId: "The ID of the SSH host to get."
|
sshHostId: "The ID of the SSH host to get."
|
||||||
@@ -1862,6 +1901,13 @@ export const AppConnections = {
|
|||||||
instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).",
|
instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).",
|
||||||
accessToken: "The access token to use to connect with Windmill."
|
accessToken: "The access token to use to connect with Windmill."
|
||||||
},
|
},
|
||||||
|
HC_VAULT: {
|
||||||
|
instanceUrl: "The Hashicrop Vault instance URL to connect with.",
|
||||||
|
namespace: "The Hashicrop Vault namespace to connect with.",
|
||||||
|
accessToken: "The access token used to connect with Hashicorp Vault.",
|
||||||
|
roleId: "The Role ID used to connect with Hashicorp Vault.",
|
||||||
|
secretId: "The Secret ID used to connect with Hashicorp Vault."
|
||||||
|
},
|
||||||
LDAP: {
|
LDAP: {
|
||||||
provider: "The type of LDAP provider. Determines provider-specific behaviors.",
|
provider: "The type of LDAP provider. Determines provider-specific behaviors.",
|
||||||
url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').",
|
url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').",
|
||||||
@@ -2019,6 +2065,10 @@ export const SecretSyncs = {
|
|||||||
workspace: "The Windmill workspace to sync secrets to.",
|
workspace: "The Windmill workspace to sync secrets to.",
|
||||||
path: "The Windmill workspace path to sync secrets to."
|
path: "The Windmill workspace path to sync secrets to."
|
||||||
},
|
},
|
||||||
|
HC_VAULT: {
|
||||||
|
mount: "The Hashicorp Vault Secrets Engine Mount to sync secrets to.",
|
||||||
|
path: "The Hashicorp Vault path to sync secrets to."
|
||||||
|
},
|
||||||
TEAMCITY: {
|
TEAMCITY: {
|
||||||
project: "The TeamCity project to sync secrets to.",
|
project: "The TeamCity project to sync secrets to.",
|
||||||
buildConfig: "The TeamCity build configuration to sync secrets to."
|
buildConfig: "The TeamCity build configuration to sync secrets to."
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns";
|
||||||
|
import { sendSlackNotification } from "@app/services/slack/slack-fns";
|
||||||
|
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import { TriggerFeature, TTriggerWorkflowNotificationDTO } from "./types";
|
||||||
|
|
||||||
|
export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkflowNotificationDTO) => {
|
||||||
|
try {
|
||||||
|
const { projectId, notification } = dto.input;
|
||||||
|
const { projectDAL, projectSlackConfigDAL, kmsService, projectMicrosoftTeamsConfigDAL, microsoftTeamsService } =
|
||||||
|
dto.dependencies;
|
||||||
|
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.getIntegrationDetailsByProject(projectId);
|
||||||
|
const slackConfig = await projectSlackConfigDAL.getIntegrationDetailsByProject(projectId);
|
||||||
|
|
||||||
|
if (slackConfig) {
|
||||||
|
if (notification.type === TriggerFeature.ACCESS_REQUEST) {
|
||||||
|
const targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || [];
|
||||||
|
if (targetChannelIds.length && slackConfig.isAccessRequestNotificationEnabled) {
|
||||||
|
await sendSlackNotification({
|
||||||
|
orgId: project.orgId,
|
||||||
|
notification,
|
||||||
|
kmsService,
|
||||||
|
targetChannelIds,
|
||||||
|
slackIntegration: slackConfig
|
||||||
|
}).catch((error) => {
|
||||||
|
logger.error(error, "Error sending Slack notification");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (notification.type === TriggerFeature.SECRET_APPROVAL) {
|
||||||
|
const targetChannelIds = slackConfig.secretRequestChannels?.split(", ") || [];
|
||||||
|
if (targetChannelIds.length && slackConfig.isSecretRequestNotificationEnabled) {
|
||||||
|
await sendSlackNotification({
|
||||||
|
orgId: project.orgId,
|
||||||
|
notification,
|
||||||
|
kmsService,
|
||||||
|
targetChannelIds,
|
||||||
|
slackIntegration: slackConfig
|
||||||
|
}).catch((error) => {
|
||||||
|
logger.error(error, "Error sending Slack notification");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (microsoftTeamsConfig) {
|
||||||
|
if (notification.type === TriggerFeature.ACCESS_REQUEST) {
|
||||||
|
if (microsoftTeamsConfig.isAccessRequestNotificationEnabled && microsoftTeamsConfig.accessRequestChannels) {
|
||||||
|
const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse(
|
||||||
|
microsoftTeamsConfig.accessRequestChannels
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success && data) {
|
||||||
|
await microsoftTeamsService
|
||||||
|
.sendNotification({
|
||||||
|
notification,
|
||||||
|
target: data,
|
||||||
|
tenantId: microsoftTeamsConfig.tenantId,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsConfig.id,
|
||||||
|
orgId: project.orgId
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(error, "Error sending Microsoft Teams notification");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (notification.type === TriggerFeature.SECRET_APPROVAL) {
|
||||||
|
if (microsoftTeamsConfig.isSecretRequestNotificationEnabled && microsoftTeamsConfig.secretRequestChannels) {
|
||||||
|
const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse(
|
||||||
|
microsoftTeamsConfig.secretRequestChannels
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success && data) {
|
||||||
|
await microsoftTeamsService
|
||||||
|
.sendNotification({
|
||||||
|
notification,
|
||||||
|
target: data,
|
||||||
|
tenantId: microsoftTeamsConfig.tenantId,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsConfig.id,
|
||||||
|
orgId: project.orgId
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(error, "Error sending Microsoft Teams notification");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Error triggering workflow integration notification");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||||
|
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||||
|
|
||||||
|
export enum TriggerFeature {
|
||||||
|
SECRET_APPROVAL = "secret-approval",
|
||||||
|
ACCESS_REQUEST = "access-request"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TNotification =
|
||||||
|
| {
|
||||||
|
type: TriggerFeature.SECRET_APPROVAL;
|
||||||
|
payload: {
|
||||||
|
userEmail: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
requestId: string;
|
||||||
|
projectId: string;
|
||||||
|
secretKeys: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: TriggerFeature.ACCESS_REQUEST;
|
||||||
|
payload: {
|
||||||
|
requesterFullName: string;
|
||||||
|
requesterEmail: string;
|
||||||
|
isTemporary: boolean;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
projectName: string;
|
||||||
|
permissions: string[];
|
||||||
|
approvalUrl: string;
|
||||||
|
note?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTriggerWorkflowNotificationDTO = {
|
||||||
|
input: {
|
||||||
|
projectId: string;
|
||||||
|
notification: TNotification;
|
||||||
|
};
|
||||||
|
dependencies: {
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
|
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
|
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -111,6 +111,11 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Authentication is handled on a route-level here.
|
||||||
|
if (req.url.includes("/api/v1/workflow-integrations/microsoft-teams/message-endpoint")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
|
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
|
||||||
|
|
||||||
if (!authMode) return;
|
if (!authMode) return;
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ import { sshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
|||||||
import { sshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
|
import { sshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
|
||||||
import { sshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service";
|
import { sshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service";
|
||||||
import { sshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
import { sshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||||
|
import { sshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal";
|
||||||
|
import { sshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-membership-dal";
|
||||||
|
import { sshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service";
|
||||||
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
||||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
@@ -174,6 +177,9 @@ import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
|
|||||||
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { microsoftTeamsIntegrationDALFactory } from "@app/services/microsoft-teams/microsoft-teams-integration-dal";
|
||||||
|
import { microsoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||||
|
import { projectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||||
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
||||||
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
import { orgDALFactory } from "@app/services/org/org-dal";
|
import { orgDALFactory } from "@app/services/org/org-dal";
|
||||||
@@ -399,6 +405,8 @@ export const registerRoutes = async (
|
|||||||
const sshHostDAL = sshHostDALFactory(db);
|
const sshHostDAL = sshHostDALFactory(db);
|
||||||
const sshHostLoginUserDAL = sshHostLoginUserDALFactory(db);
|
const sshHostLoginUserDAL = sshHostLoginUserDALFactory(db);
|
||||||
const sshHostLoginUserMappingDAL = sshHostLoginUserMappingDALFactory(db);
|
const sshHostLoginUserMappingDAL = sshHostLoginUserMappingDALFactory(db);
|
||||||
|
const sshHostGroupDAL = sshHostGroupDALFactory(db);
|
||||||
|
const sshHostGroupMembershipDAL = sshHostGroupMembershipDALFactory(db);
|
||||||
|
|
||||||
const kmsDAL = kmskeyDALFactory(db);
|
const kmsDAL = kmskeyDALFactory(db);
|
||||||
const internalKmsDAL = internalKmsDALFactory(db);
|
const internalKmsDAL = internalKmsDALFactory(db);
|
||||||
@@ -426,6 +434,8 @@ export const registerRoutes = async (
|
|||||||
const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
|
const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
|
||||||
|
|
||||||
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
|
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
|
||||||
|
const microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDALFactory(db);
|
||||||
|
const projectMicrosoftTeamsConfigDAL = projectMicrosoftTeamsConfigDALFactory(db);
|
||||||
|
|
||||||
const permissionService = permissionServiceFactory({
|
const permissionService = permissionServiceFactory({
|
||||||
permissionDAL,
|
permissionDAL,
|
||||||
@@ -623,6 +633,7 @@ export const registerRoutes = async (
|
|||||||
tokenService,
|
tokenService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
totpService,
|
totpService,
|
||||||
|
orgMembershipDAL,
|
||||||
auditLogService
|
auditLogService
|
||||||
});
|
});
|
||||||
const passwordService = authPaswordServiceFactory({
|
const passwordService = authPaswordServiceFactory({
|
||||||
@@ -687,6 +698,15 @@ export const registerRoutes = async (
|
|||||||
orgDAL,
|
orgDAL,
|
||||||
externalGroupOrgRoleMappingDAL
|
externalGroupOrgRoleMappingDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const microsoftTeamsService = microsoftTeamsServiceFactory({
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
permissionService,
|
||||||
|
workflowIntegrationDAL,
|
||||||
|
kmsService,
|
||||||
|
serverCfgDAL: superAdminDAL
|
||||||
|
});
|
||||||
|
|
||||||
const superAdminService = superAdminServiceFactory({
|
const superAdminService = superAdminServiceFactory({
|
||||||
userDAL,
|
userDAL,
|
||||||
identityDAL,
|
identityDAL,
|
||||||
@@ -700,7 +720,8 @@ export const registerRoutes = async (
|
|||||||
orgService,
|
orgService,
|
||||||
keyStore,
|
keyStore,
|
||||||
licenseService,
|
licenseService,
|
||||||
kmsService
|
kmsService,
|
||||||
|
microsoftTeamsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const orgAdminService = orgAdminServiceFactory({
|
const orgAdminService = orgAdminServiceFactory({
|
||||||
@@ -851,6 +872,19 @@ export const registerRoutes = async (
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sshHostGroupService = sshHostGroupServiceFactory({
|
||||||
|
projectDAL,
|
||||||
|
sshHostDAL,
|
||||||
|
sshHostGroupDAL,
|
||||||
|
sshHostGroupMembershipDAL,
|
||||||
|
sshHostLoginUserDAL,
|
||||||
|
sshHostLoginUserMappingDAL,
|
||||||
|
userDAL,
|
||||||
|
permissionService,
|
||||||
|
licenseService,
|
||||||
|
groupDAL
|
||||||
|
});
|
||||||
|
|
||||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
@@ -1020,6 +1054,7 @@ export const registerRoutes = async (
|
|||||||
sshCertificateDAL,
|
sshCertificateDAL,
|
||||||
sshCertificateTemplateDAL,
|
sshCertificateTemplateDAL,
|
||||||
sshHostDAL,
|
sshHostDAL,
|
||||||
|
sshHostGroupDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
@@ -1028,6 +1063,8 @@ export const registerRoutes = async (
|
|||||||
certificateTemplateDAL,
|
certificateTemplateDAL,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
slackIntegrationDAL,
|
slackIntegrationDAL,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
projectTemplateService,
|
projectTemplateService,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
smtpService
|
smtpService
|
||||||
@@ -1152,7 +1189,9 @@ export const registerRoutes = async (
|
|||||||
userDAL,
|
userDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
|
microsoftTeamsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretService = secretServiceFactory({
|
const secretService = secretServiceFactory({
|
||||||
@@ -1214,7 +1253,9 @@ export const registerRoutes = async (
|
|||||||
accessApprovalPolicyApproverDAL,
|
accessApprovalPolicyApproverDAL,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
groupDAL
|
groupDAL,
|
||||||
|
microsoftTeamsService,
|
||||||
|
projectMicrosoftTeamsConfigDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretReplicationService = secretReplicationServiceFactory({
|
const secretReplicationService = secretReplicationServiceFactory({
|
||||||
@@ -1612,6 +1653,7 @@ export const registerRoutes = async (
|
|||||||
await dailyResourceCleanUp.startCleanUp();
|
await dailyResourceCleanUp.startCleanUp();
|
||||||
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
||||||
await kmsService.startService();
|
await kmsService.startService();
|
||||||
|
await microsoftTeamsService.start();
|
||||||
|
|
||||||
// inject all services
|
// inject all services
|
||||||
server.decorate<FastifyZodProvider["services"]>("services", {
|
server.decorate<FastifyZodProvider["services"]>("services", {
|
||||||
@@ -1671,6 +1713,7 @@ export const registerRoutes = async (
|
|||||||
sshCertificateAuthority: sshCertificateAuthorityService,
|
sshCertificateAuthority: sshCertificateAuthorityService,
|
||||||
sshCertificateTemplate: sshCertificateTemplateService,
|
sshCertificateTemplate: sshCertificateTemplateService,
|
||||||
sshHost: sshHostService,
|
sshHost: sshHostService,
|
||||||
|
sshHostGroup: sshHostGroupService,
|
||||||
certificateAuthority: certificateAuthorityService,
|
certificateAuthority: certificateAuthorityService,
|
||||||
certificateTemplate: certificateTemplateService,
|
certificateTemplate: certificateTemplateService,
|
||||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||||
@@ -1704,6 +1747,7 @@ export const registerRoutes = async (
|
|||||||
kmipOperation: kmipOperationService,
|
kmipOperation: kmipOperationService,
|
||||||
gateway: gatewayService,
|
gateway: gatewayService,
|
||||||
secretRotationV2: secretRotationV2Service,
|
secretRotationV2: secretRotationV2Service,
|
||||||
|
microsoftTeams: microsoftTeamsService,
|
||||||
assumePrivileges: assumePrivilegeService,
|
assumePrivileges: assumePrivilegeService,
|
||||||
githubOrgSync: githubOrgSyncConfigService
|
githubOrgSync: githubOrgSyncConfigService
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
encryptedSlackClientId: true,
|
encryptedSlackClientId: true,
|
||||||
encryptedSlackClientSecret: true
|
encryptedSlackClientSecret: true,
|
||||||
|
encryptedMicrosoftTeamsAppId: true,
|
||||||
|
encryptedMicrosoftTeamsClientSecret: true,
|
||||||
|
encryptedMicrosoftTeamsBotId: true
|
||||||
}).extend({
|
}).extend({
|
||||||
isMigrationModeOn: z.boolean(),
|
isMigrationModeOn: z.boolean(),
|
||||||
defaultAuthOrgSlug: z.string().nullable(),
|
defaultAuthOrgSlug: z.string().nullable(),
|
||||||
@@ -74,6 +77,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
slackClientId: z.string().optional(),
|
slackClientId: z.string().optional(),
|
||||||
slackClientSecret: z.string().optional(),
|
slackClientSecret: z.string().optional(),
|
||||||
|
microsoftTeamsAppId: z.string().optional(),
|
||||||
|
microsoftTeamsClientSecret: z.string().optional(),
|
||||||
|
microsoftTeamsBotId: z.string().optional(),
|
||||||
authConsentContent: z
|
authConsentContent: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -197,15 +203,22 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/integrations/slack/config",
|
url: "/integrations",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
clientId: z.string(),
|
slack: z.object({
|
||||||
clientSecret: z.string()
|
clientId: z.string(),
|
||||||
|
clientSecret: z.string()
|
||||||
|
}),
|
||||||
|
microsoftTeams: z.object({
|
||||||
|
appId: z.string(),
|
||||||
|
clientSecret: z.string(),
|
||||||
|
botId: z.string()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -215,9 +228,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
const adminSlackConfig = await server.services.superAdmin.getAdminSlackConfig();
|
const adminIntegrationsConfig = await server.services.superAdmin.getAdminIntegrationsConfig();
|
||||||
|
|
||||||
return adminSlackConfig;
|
return adminIntegrationsConfig;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ import {
|
|||||||
} from "@app/services/app-connection/databricks";
|
} from "@app/services/app-connection/databricks";
|
||||||
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
|
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
|
||||||
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||||
|
import {
|
||||||
|
HCVaultConnectionListItemSchema,
|
||||||
|
SanitizedHCVaultConnectionSchema
|
||||||
|
} from "@app/services/app-connection/hc-vault";
|
||||||
import {
|
import {
|
||||||
HumanitecConnectionListItemSchema,
|
HumanitecConnectionListItemSchema,
|
||||||
SanitizedHumanitecConnectionSchema
|
SanitizedHumanitecConnectionSchema
|
||||||
@@ -68,6 +72,7 @@ const SanitizedAppConnectionSchema = z.union([
|
|||||||
...SanitizedMsSqlConnectionSchema.options,
|
...SanitizedMsSqlConnectionSchema.options,
|
||||||
...SanitizedCamundaConnectionSchema.options,
|
...SanitizedCamundaConnectionSchema.options,
|
||||||
...SanitizedAuth0ConnectionSchema.options,
|
...SanitizedAuth0ConnectionSchema.options,
|
||||||
|
...SanitizedHCVaultConnectionSchema.options,
|
||||||
...SanitizedAzureClientSecretsConnectionSchema.options,
|
...SanitizedAzureClientSecretsConnectionSchema.options,
|
||||||
...SanitizedWindmillConnectionSchema.options,
|
...SanitizedWindmillConnectionSchema.options,
|
||||||
...SanitizedLdapConnectionSchema.options,
|
...SanitizedLdapConnectionSchema.options,
|
||||||
@@ -88,6 +93,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
|||||||
MsSqlConnectionListItemSchema,
|
MsSqlConnectionListItemSchema,
|
||||||
CamundaConnectionListItemSchema,
|
CamundaConnectionListItemSchema,
|
||||||
Auth0ConnectionListItemSchema,
|
Auth0ConnectionListItemSchema,
|
||||||
|
HCVaultConnectionListItemSchema,
|
||||||
AzureClientSecretsConnectionListItemSchema,
|
AzureClientSecretsConnectionListItemSchema,
|
||||||
WindmillConnectionListItemSchema,
|
WindmillConnectionListItemSchema,
|
||||||
LdapConnectionListItemSchema,
|
LdapConnectionListItemSchema,
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateHCVaultConnectionSchema,
|
||||||
|
SanitizedHCVaultConnectionSchema,
|
||||||
|
UpdateHCVaultConnectionSchema
|
||||||
|
} from "@app/services/app-connection/hc-vault";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerHCVaultConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.HCVault,
|
||||||
|
server,
|
||||||
|
sanitizedResponseSchema: SanitizedHCVaultConnectionSchema,
|
||||||
|
createSchema: CreateHCVaultConnectionSchema,
|
||||||
|
updateSchema: UpdateHCVaultConnectionSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
// The following endpoints are for internal Infisical App use only and not part of the public API
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/mounts`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.string().array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const mounts = await server.services.appConnection.hcvault.listMounts(connectionId, req.permission);
|
||||||
|
return mounts;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
|||||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||||
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||||
|
import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router";
|
||||||
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
||||||
import { registerLdapConnectionRouter } from "./ldap-connection-router";
|
import { registerLdapConnectionRouter } from "./ldap-connection-router";
|
||||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||||
@@ -37,6 +38,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
|||||||
[AppConnection.Camunda]: registerCamundaConnectionRouter,
|
[AppConnection.Camunda]: registerCamundaConnectionRouter,
|
||||||
[AppConnection.Windmill]: registerWindmillConnectionRouter,
|
[AppConnection.Windmill]: registerWindmillConnectionRouter,
|
||||||
[AppConnection.Auth0]: registerAuth0ConnectionRouter,
|
[AppConnection.Auth0]: registerAuth0ConnectionRouter,
|
||||||
|
[AppConnection.HCVault]: registerHCVaultConnectionRouter,
|
||||||
[AppConnection.LDAP]: registerLdapConnectionRouter,
|
[AppConnection.LDAP]: registerLdapConnectionRouter,
|
||||||
[AppConnection.TeamCity]: registerTeamCityConnectionRouter
|
[AppConnection.TeamCity]: registerTeamCityConnectionRouter
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { registerIdentityUaRouter } from "./identity-universal-auth-router";
|
|||||||
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
||||||
import { registerIntegrationRouter } from "./integration-router";
|
import { registerIntegrationRouter } from "./integration-router";
|
||||||
import { registerInviteOrgRouter } from "./invite-org-router";
|
import { registerInviteOrgRouter } from "./invite-org-router";
|
||||||
|
import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router";
|
||||||
import { registerOrgAdminRouter } from "./org-admin-router";
|
import { registerOrgAdminRouter } from "./org-admin-router";
|
||||||
import { registerOrgRouter } from "./organization-router";
|
import { registerOrgRouter } from "./organization-router";
|
||||||
import { registerPasswordRouter } from "./password-router";
|
import { registerPasswordRouter } from "./password-router";
|
||||||
@@ -79,6 +80,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
async (workflowIntegrationRouter) => {
|
async (workflowIntegrationRouter) => {
|
||||||
await workflowIntegrationRouter.register(registerWorkflowIntegrationRouter);
|
await workflowIntegrationRouter.register(registerWorkflowIntegrationRouter);
|
||||||
await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" });
|
await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" });
|
||||||
|
await workflowIntegrationRouter.register(registerMicrosoftTeamsRouter, { prefix: "/microsoft-teams" });
|
||||||
},
|
},
|
||||||
{ prefix: "/workflow-integrations" }
|
{ prefix: "/workflow-integrations" }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { MicrosoftTeamsIntegrationsSchema, WorkflowIntegrationsSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { WorkflowIntegrationStatus } from "@app/services/workflow-integration/workflow-integration-types";
|
||||||
|
|
||||||
|
const sanitizedMicrosoftTeamsIntegrationSchema = WorkflowIntegrationsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
description: true,
|
||||||
|
slug: true,
|
||||||
|
integration: true
|
||||||
|
}).merge(
|
||||||
|
MicrosoftTeamsIntegrationsSchema.pick({
|
||||||
|
tenantId: true
|
||||||
|
}).extend({
|
||||||
|
status: z.nativeEnum(WorkflowIntegrationStatus)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/client-id",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
clientId: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const clientId = await server.services.microsoftTeams.getClientId({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
clientId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
redirectUri: z.string(),
|
||||||
|
tenantId: z.string().uuid(),
|
||||||
|
slug: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
code: z.string().trim()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.microsoftTeams.completeMicrosoftTeamsIntegration({
|
||||||
|
tenantId: req.body.tenantId,
|
||||||
|
slug: req.body.slug,
|
||||||
|
description: req.body.description,
|
||||||
|
redirectUri: req.body.redirectUri,
|
||||||
|
code: req.body.code,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE,
|
||||||
|
metadata: {
|
||||||
|
tenantId: req.body.tenantId,
|
||||||
|
slug: req.body.slug,
|
||||||
|
description: req.body.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
response: {
|
||||||
|
200: sanitizedMicrosoftTeamsIntegrationSchema.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const microsoftTeamsIntegrations = await server.services.microsoftTeams.getMicrosoftTeamsIntegrationsByOrg({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST,
|
||||||
|
metadata: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return microsoftTeamsIntegrations;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:id/installation-status",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const microsoftTeamsIntegration = await server.services.microsoftTeams.checkInstallationStatus({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
workflowIntegrationId: req.params.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS,
|
||||||
|
metadata: {
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
|
slug: microsoftTeamsIntegration.slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedMicrosoftTeamsIntegrationSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const deletedMicrosoftTeamsIntegration = await server.services.microsoftTeams.deleteMicrosoftTeamsIntegration({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE,
|
||||||
|
metadata: {
|
||||||
|
tenantId: deletedMicrosoftTeamsIntegration.tenantId,
|
||||||
|
slug: deletedMicrosoftTeamsIntegration.slug,
|
||||||
|
id: deletedMicrosoftTeamsIntegration.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return deletedMicrosoftTeamsIntegration;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedMicrosoftTeamsIntegrationSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const microsoftTeamsIntegration = await server.services.microsoftTeams.getMicrosoftTeamsIntegrationById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET,
|
||||||
|
metadata: {
|
||||||
|
slug: microsoftTeamsIntegration.slug,
|
||||||
|
id: microsoftTeamsIntegration.id,
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return microsoftTeamsIntegration;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
slug: slugSchema({ max: 64 }).optional(),
|
||||||
|
description: z.string().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedMicrosoftTeamsIntegrationSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const microsoftTeamsIntegration = await server.services.microsoftTeams.updateMicrosoftTeamsIntegration({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.id,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE,
|
||||||
|
metadata: {
|
||||||
|
slug: microsoftTeamsIntegration.slug,
|
||||||
|
id: microsoftTeamsIntegration.id,
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
|
newSlug: req.body.slug,
|
||||||
|
newDescription: req.body.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return microsoftTeamsIntegration;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:workflowIntegrationId/teams",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workflowIntegrationId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z
|
||||||
|
.object({
|
||||||
|
teamId: z.string(),
|
||||||
|
teamName: z.string(),
|
||||||
|
channels: z
|
||||||
|
.object({
|
||||||
|
channelName: z.string(),
|
||||||
|
channelId: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const microsoftTeamsIntegration = await server.services.microsoftTeams.getTeams({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
workflowIntegrationId: req.params.workflowIntegrationId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS,
|
||||||
|
metadata: {
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
|
slug: microsoftTeamsIntegration.slug,
|
||||||
|
id: microsoftTeamsIntegration.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return microsoftTeamsIntegration.teams;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/message-endpoint",
|
||||||
|
schema: {
|
||||||
|
body: z.any(),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
await server.services.microsoftTeams.handleMessageEndpoint(req, res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
UserEncryptionKeysSchema,
|
UserEncryptionKeysSchema,
|
||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import { ProjectMicrosoftTeamsConfigsSchema } from "@app/db/schemas/project-microsoft-teams-configs";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs";
|
import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs";
|
||||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
@@ -21,8 +22,10 @@ import { re2Validator } from "@app/lib/zod";
|
|||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns";
|
||||||
import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types";
|
import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types";
|
||||||
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators";
|
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators";
|
||||||
|
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
|
||||||
|
|
||||||
import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
|
import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||||
import { sanitizedServiceTokenSchema } from "../v2/service-token-router";
|
import { sanitizedServiceTokenSchema } from "../v2/service-token-router";
|
||||||
@@ -740,55 +743,112 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/slack-config",
|
url: "/:workspaceId/workflow-integration-config/:integration",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim(),
|
||||||
|
integration: z.nativeEnum(WorkflowIntegration)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: ProjectSlackConfigsSchema.pick({
|
200: z.discriminatedUnion("integration", [
|
||||||
id: true,
|
ProjectSlackConfigsSchema.pick({
|
||||||
slackIntegrationId: true,
|
id: true,
|
||||||
isAccessRequestNotificationEnabled: true,
|
isAccessRequestNotificationEnabled: true,
|
||||||
accessRequestChannels: true,
|
accessRequestChannels: true,
|
||||||
isSecretRequestNotificationEnabled: true,
|
isSecretRequestNotificationEnabled: true,
|
||||||
secretRequestChannels: true
|
secretRequestChannels: true
|
||||||
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
integration: z.literal(WorkflowIntegration.SLACK),
|
||||||
|
integrationId: z.string()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
ProjectMicrosoftTeamsConfigsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
isAccessRequestNotificationEnabled: true,
|
||||||
|
accessRequestChannels: true,
|
||||||
|
isSecretRequestNotificationEnabled: true,
|
||||||
|
secretRequestChannels: true
|
||||||
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
|
||||||
|
integrationId: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const config = await server.services.project.getProjectWorkflowIntegrationConfig({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
integration: req.params.integration
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG,
|
||||||
|
metadata: {
|
||||||
|
id: config.id,
|
||||||
|
integration: config.integration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:projectId/workflow-integration/:integration/:integrationId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
integration: z.nativeEnum(WorkflowIntegration),
|
||||||
|
integrationId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
integrationConfig: z.object({
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const slackConfig = await server.services.project.getProjectSlackConfig({
|
const deletedIntegration = await server.services.project.deleteProjectWorkflowIntegration({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.workspaceId
|
projectId: req.params.projectId,
|
||||||
|
integration: req.params.integration,
|
||||||
|
integrationId: req.params.integrationId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (slackConfig) {
|
return {
|
||||||
await server.services.auditLog.createAuditLog({
|
integrationConfig: deletedIntegration
|
||||||
...req.auditLogInfo,
|
};
|
||||||
projectId: req.params.workspaceId,
|
|
||||||
event: {
|
|
||||||
type: EventType.GET_PROJECT_SLACK_CONFIG,
|
|
||||||
metadata: {
|
|
||||||
id: slackConfig.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return slackConfig;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
url: "/:workspaceId/slack-config",
|
url: "/:workspaceId/workflow-integration",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
@@ -796,27 +856,57 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
|
||||||
slackIntegrationId: z.string(),
|
body: z.discriminatedUnion("integration", [
|
||||||
isAccessRequestNotificationEnabled: z.boolean(),
|
z.object({
|
||||||
accessRequestChannels: validateSlackChannelsField,
|
integration: z.literal(WorkflowIntegration.SLACK),
|
||||||
isSecretRequestNotificationEnabled: z.boolean(),
|
integrationId: z.string(),
|
||||||
secretRequestChannels: validateSlackChannelsField
|
accessRequestChannels: validateSlackChannelsField,
|
||||||
}),
|
secretRequestChannels: validateSlackChannelsField,
|
||||||
response: {
|
isAccessRequestNotificationEnabled: z.boolean(),
|
||||||
200: ProjectSlackConfigsSchema.pick({
|
isSecretRequestNotificationEnabled: z.boolean()
|
||||||
id: true,
|
}),
|
||||||
slackIntegrationId: true,
|
z.object({
|
||||||
isAccessRequestNotificationEnabled: true,
|
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
|
||||||
accessRequestChannels: true,
|
integrationId: z.string(),
|
||||||
isSecretRequestNotificationEnabled: true,
|
accessRequestChannels: validateMicrosoftTeamsChannelsSchema,
|
||||||
secretRequestChannels: true
|
secretRequestChannels: validateMicrosoftTeamsChannelsSchema,
|
||||||
|
isAccessRequestNotificationEnabled: z.boolean(),
|
||||||
|
isSecretRequestNotificationEnabled: z.boolean()
|
||||||
})
|
})
|
||||||
|
]),
|
||||||
|
response: {
|
||||||
|
200: z.discriminatedUnion("integration", [
|
||||||
|
ProjectSlackConfigsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
isAccessRequestNotificationEnabled: true,
|
||||||
|
accessRequestChannels: true,
|
||||||
|
isSecretRequestNotificationEnabled: true,
|
||||||
|
secretRequestChannels: true
|
||||||
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
integration: z.literal(WorkflowIntegration.SLACK),
|
||||||
|
integrationId: z.string()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
ProjectMicrosoftTeamsConfigsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
isAccessRequestNotificationEnabled: true,
|
||||||
|
isSecretRequestNotificationEnabled: true
|
||||||
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
|
||||||
|
integrationId: z.string(),
|
||||||
|
accessRequestChannels: validateMicrosoftTeamsChannelsSchema,
|
||||||
|
secretRequestChannels: validateMicrosoftTeamsChannelsSchema
|
||||||
|
})
|
||||||
|
)
|
||||||
|
])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const slackConfig = await server.services.project.updateProjectSlackConfig({
|
const workflowIntegrationConfig = await server.services.project.updateProjectWorkflowIntegration({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -829,19 +919,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.UPDATE_PROJECT_SLACK_CONFIG,
|
type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG,
|
||||||
metadata: {
|
metadata: {
|
||||||
id: slackConfig.id,
|
id: workflowIntegrationConfig.id,
|
||||||
slackIntegrationId: slackConfig.slackIntegrationId,
|
integrationId: workflowIntegrationConfig.integrationId,
|
||||||
isAccessRequestNotificationEnabled: slackConfig.isAccessRequestNotificationEnabled,
|
integration: workflowIntegrationConfig.integration,
|
||||||
accessRequestChannels: slackConfig.accessRequestChannels,
|
isAccessRequestNotificationEnabled: workflowIntegrationConfig.isAccessRequestNotificationEnabled,
|
||||||
isSecretRequestNotificationEnabled: slackConfig.isSecretRequestNotificationEnabled,
|
accessRequestChannels: workflowIntegrationConfig.accessRequestChannels,
|
||||||
secretRequestChannels: slackConfig.secretRequestChannels
|
isSecretRequestNotificationEnabled: workflowIntegrationConfig.isSecretRequestNotificationEnabled,
|
||||||
|
secretRequestChannels: workflowIntegrationConfig.secretRequestChannels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return slackConfig;
|
return workflowIntegrationConfig;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
CreateHCVaultSyncSchema,
|
||||||
|
HCVaultSyncSchema,
|
||||||
|
UpdateHCVaultSyncSchema
|
||||||
|
} from "@app/services/secret-sync/hc-vault";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
|
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||||
|
|
||||||
|
export const registerHCVaultSyncRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSyncSecretsEndpoints({
|
||||||
|
destination: SecretSync.HCVault,
|
||||||
|
server,
|
||||||
|
responseSchema: HCVaultSyncSchema,
|
||||||
|
createSchema: CreateHCVaultSyncSchema,
|
||||||
|
updateSchema: UpdateHCVaultSyncSchema
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@ import { registerCamundaSyncRouter } from "./camunda-sync-router";
|
|||||||
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
||||||
import { registerGcpSyncRouter } from "./gcp-sync-router";
|
import { registerGcpSyncRouter } from "./gcp-sync-router";
|
||||||
import { registerGitHubSyncRouter } from "./github-sync-router";
|
import { registerGitHubSyncRouter } from "./github-sync-router";
|
||||||
|
import { registerHCVaultSyncRouter } from "./hc-vault-sync-router";
|
||||||
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
||||||
import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
|
import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
|
||||||
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
|
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
|
||||||
@@ -29,5 +30,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
|||||||
[SecretSync.Camunda]: registerCamundaSyncRouter,
|
[SecretSync.Camunda]: registerCamundaSyncRouter,
|
||||||
[SecretSync.Vercel]: registerVercelSyncRouter,
|
[SecretSync.Vercel]: registerVercelSyncRouter,
|
||||||
[SecretSync.Windmill]: registerWindmillSyncRouter,
|
[SecretSync.Windmill]: registerWindmillSyncRouter,
|
||||||
|
[SecretSync.HCVault]: registerHCVaultSyncRouter,
|
||||||
[SecretSync.TeamCity]: registerTeamCitySyncRouter
|
[SecretSync.TeamCity]: registerTeamCitySyncRouter
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secr
|
|||||||
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
|
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
|
||||||
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
||||||
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
||||||
|
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
|
||||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||||
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
|
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
|
||||||
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
||||||
@@ -41,6 +42,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
|||||||
CamundaSyncSchema,
|
CamundaSyncSchema,
|
||||||
VercelSyncSchema,
|
VercelSyncSchema,
|
||||||
WindmillSyncSchema,
|
WindmillSyncSchema,
|
||||||
|
HCVaultSyncSchema,
|
||||||
TeamCitySyncSchema
|
TeamCitySyncSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -57,6 +59,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
|||||||
CamundaSyncListItemSchema,
|
CamundaSyncListItemSchema,
|
||||||
VercelSyncListItemSchema,
|
VercelSyncListItemSchema,
|
||||||
WindmillSyncListItemSchema,
|
WindmillSyncListItemSchema,
|
||||||
|
HCVaultSyncListItemSchema,
|
||||||
TeamCitySyncListItemSchema
|
TeamCitySyncListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { fetchGithubEmails, fetchGithubUser } from "@app/lib/requests/github";
|
|||||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { AuthMethod } from "@app/services/auth/auth-type";
|
import { AuthMethod } from "@app/services/auth/auth-type";
|
||||||
import { OrgAuthMethod } from "@app/services/org/org-types";
|
import { OrgAuthMethod } from "@app/services/org/org-types";
|
||||||
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
|
|
||||||
export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -342,8 +343,12 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serverCfg = await getServerCfg();
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
`${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
|
`${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}${
|
||||||
|
serverCfg.defaultAuthOrgId && !appCfg.isCloud ? `&defaultOrgAllowed=true` : ""
|
||||||
|
}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ const sanitizedWorkflowIntegrationSchema = WorkflowIntegrationsSchema.pick({
|
|||||||
id: true,
|
id: true,
|
||||||
description: true,
|
description: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
integration: true
|
integration: true,
|
||||||
|
status: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const registerWorkflowIntegrationRouter = async (server: FastifyZodProvider) => {
|
export const registerWorkflowIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-s
|
|||||||
import { sanitizedSshCertificate } from "@app/ee/services/ssh-certificate/ssh-certificate-schema";
|
import { sanitizedSshCertificate } from "@app/ee/services/ssh-certificate/ssh-certificate-schema";
|
||||||
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
||||||
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
|
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
|
||||||
|
import { LoginMappingSource } from "@app/ee/services/ssh-host/ssh-host-types";
|
||||||
|
import { sanitizedSshHostGroup } from "@app/ee/services/ssh-host-group/ssh-host-group-schema";
|
||||||
import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs";
|
import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
@@ -631,7 +633,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
hosts: z.array(
|
hosts: z.array(
|
||||||
sanitizedSshHost.extend({
|
sanitizedSshHost.extend({
|
||||||
loginMappings: z.array(loginMappingSchema)
|
loginMappings: loginMappingSchema
|
||||||
|
.extend({
|
||||||
|
source: z.nativeEnum(LoginMappingSource)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -650,4 +656,39 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
return { hosts };
|
return { hosts };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectId/ssh-host-groups",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOST_GROUPS.projectId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
groups: z.array(
|
||||||
|
sanitizedSshHostGroup.extend({
|
||||||
|
loginMappings: loginMappingSchema.array(),
|
||||||
|
hostCount: z.number()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const groups = await server.services.project.listProjectSshHostGroups({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { groups };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,24 +88,41 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: authRateLimit
|
rateLimit: authRateLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z
|
||||||
email: z.string().trim(),
|
.object({
|
||||||
firstName: z.string().trim(),
|
email: z.string().trim(),
|
||||||
lastName: z.string().trim().optional(),
|
firstName: z.string().trim(),
|
||||||
protectedKey: z.string().trim(),
|
lastName: z.string().trim().optional(),
|
||||||
protectedKeyIV: z.string().trim(),
|
protectedKey: z.string().trim(),
|
||||||
protectedKeyTag: z.string().trim(),
|
protectedKeyIV: z.string().trim(),
|
||||||
publicKey: z.string().trim(),
|
protectedKeyTag: z.string().trim(),
|
||||||
encryptedPrivateKey: z.string().trim(),
|
publicKey: z.string().trim(),
|
||||||
encryptedPrivateKeyIV: z.string().trim(),
|
encryptedPrivateKey: z.string().trim(),
|
||||||
encryptedPrivateKeyTag: z.string().trim(),
|
encryptedPrivateKeyIV: z.string().trim(),
|
||||||
salt: z.string().trim(),
|
encryptedPrivateKeyTag: z.string().trim(),
|
||||||
verifier: z.string().trim(),
|
salt: z.string().trim(),
|
||||||
organizationName: GenericResourceNameSchema,
|
verifier: z.string().trim(),
|
||||||
providerAuthToken: z.string().trim().optional().nullish(),
|
providerAuthToken: z.string().trim().optional().nullish(),
|
||||||
attributionSource: z.string().trim().optional(),
|
attributionSource: z.string().trim().optional(),
|
||||||
password: z.string()
|
password: z.string()
|
||||||
}),
|
})
|
||||||
|
.and(
|
||||||
|
z.preprocess(
|
||||||
|
(data) => {
|
||||||
|
if (typeof data === "object" && data && "useDefaultOrg" in data === false) {
|
||||||
|
return { ...data, useDefaultOrg: false };
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
z.discriminatedUnion("useDefaultOrg", [
|
||||||
|
z.object({ useDefaultOrg: z.literal(true) }),
|
||||||
|
z.object({
|
||||||
|
useDefaultOrg: z.literal(false),
|
||||||
|
organizationName: GenericResourceNameSchema
|
||||||
|
})
|
||||||
|
])
|
||||||
|
)
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export enum AppConnection {
|
|||||||
Camunda = "camunda",
|
Camunda = "camunda",
|
||||||
Windmill = "windmill",
|
Windmill = "windmill",
|
||||||
Auth0 = "auth0",
|
Auth0 = "auth0",
|
||||||
|
HCVault = "hashicorp-vault",
|
||||||
LDAP = "ldap",
|
LDAP = "ldap",
|
||||||
TeamCity = "teamcity"
|
TeamCity = "teamcity"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ import {
|
|||||||
} from "./databricks";
|
} from "./databricks";
|
||||||
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
|
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
|
||||||
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
|
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
|
||||||
|
import {
|
||||||
|
getHCVaultConnectionListItem,
|
||||||
|
HCVaultConnectionMethod,
|
||||||
|
validateHCVaultConnectionCredentials
|
||||||
|
} from "./hc-vault";
|
||||||
import {
|
import {
|
||||||
getHumanitecConnectionListItem,
|
getHumanitecConnectionListItem,
|
||||||
HumanitecConnectionMethod,
|
HumanitecConnectionMethod,
|
||||||
@@ -84,6 +89,7 @@ export const listAppConnectionOptions = () => {
|
|||||||
getAzureClientSecretsConnectionListItem(),
|
getAzureClientSecretsConnectionListItem(),
|
||||||
getWindmillConnectionListItem(),
|
getWindmillConnectionListItem(),
|
||||||
getAuth0ConnectionListItem(),
|
getAuth0ConnectionListItem(),
|
||||||
|
getHCVaultConnectionListItem(),
|
||||||
getLdapConnectionListItem(),
|
getLdapConnectionListItem(),
|
||||||
getTeamCityConnectionListItem()
|
getTeamCityConnectionListItem()
|
||||||
].sort((a, b) => a.name.localeCompare(b.name));
|
].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
@@ -152,6 +158,7 @@ export const validateAppConnectionCredentials = async (
|
|||||||
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator
|
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator
|
||||||
};
|
};
|
||||||
@@ -186,10 +193,13 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||||
return "Username & Password";
|
return "Username & Password";
|
||||||
case WindmillConnectionMethod.AccessToken:
|
case WindmillConnectionMethod.AccessToken:
|
||||||
|
case HCVaultConnectionMethod.AccessToken:
|
||||||
case TeamCityConnectionMethod.AccessToken:
|
case TeamCityConnectionMethod.AccessToken:
|
||||||
return "Access Token";
|
return "Access Token";
|
||||||
case Auth0ConnectionMethod.ClientCredentials:
|
case Auth0ConnectionMethod.ClientCredentials:
|
||||||
return "Client Credentials";
|
return "Client Credentials";
|
||||||
|
case HCVaultConnectionMethod.AppRole:
|
||||||
|
return "App Role";
|
||||||
case LdapConnectionMethod.SimpleBind:
|
case LdapConnectionMethod.SimpleBind:
|
||||||
return "Simple Bind";
|
return "Simple Bind";
|
||||||
default:
|
default:
|
||||||
@@ -238,6 +248,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
|||||||
[AppConnection.AzureClientSecrets]: platformManagedCredentialsNotSupported,
|
[AppConnection.AzureClientSecrets]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
|
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
|
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
|
||||||
|
[AppConnection.HCVault]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
|
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
|
||||||
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
|
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
|||||||
[AppConnection.Camunda]: "Camunda",
|
[AppConnection.Camunda]: "Camunda",
|
||||||
[AppConnection.Windmill]: "Windmill",
|
[AppConnection.Windmill]: "Windmill",
|
||||||
[AppConnection.Auth0]: "Auth0",
|
[AppConnection.Auth0]: "Auth0",
|
||||||
|
[AppConnection.HCVault]: "Hashicorp Vault",
|
||||||
[AppConnection.LDAP]: "LDAP",
|
[AppConnection.LDAP]: "LDAP",
|
||||||
[AppConnection.TeamCity]: "TeamCity"
|
[AppConnection.TeamCity]: "TeamCity"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
|||||||
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||||
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||||
import { githubConnectionService } from "./github/github-connection-service";
|
import { githubConnectionService } from "./github/github-connection-service";
|
||||||
|
import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault";
|
||||||
|
import { hcVaultConnectionService } from "./hc-vault/hc-vault-connection-service";
|
||||||
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
|
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
|
||||||
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
|
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
|
||||||
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
|
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
|
||||||
@@ -81,6 +83,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
|||||||
[AppConnection.AzureClientSecrets]: ValidateAzureClientSecretsConnectionCredentialsSchema,
|
[AppConnection.AzureClientSecrets]: ValidateAzureClientSecretsConnectionCredentialsSchema,
|
||||||
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
|
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
|
||||||
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
|
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
|
||||||
|
[AppConnection.HCVault]: ValidateHCVaultConnectionCredentialsSchema,
|
||||||
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
|
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
|
||||||
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
|
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
|
||||||
};
|
};
|
||||||
@@ -459,6 +462,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
vercel: vercelConnectionService(connectAppConnectionById),
|
vercel: vercelConnectionService(connectAppConnectionById),
|
||||||
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
|
hcvault: hcVaultConnectionService(connectAppConnectionById),
|
||||||
windmill: windmillConnectionService(connectAppConnectionById),
|
windmill: windmillConnectionService(connectAppConnectionById),
|
||||||
teamcity: teamcityConnectionService(connectAppConnectionById)
|
teamcity: teamcityConnectionService(connectAppConnectionById)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,6 +57,12 @@ import {
|
|||||||
TGitHubConnectionInput,
|
TGitHubConnectionInput,
|
||||||
TValidateGitHubConnectionCredentialsSchema
|
TValidateGitHubConnectionCredentialsSchema
|
||||||
} from "./github";
|
} from "./github";
|
||||||
|
import {
|
||||||
|
THCVaultConnection,
|
||||||
|
THCVaultConnectionConfig,
|
||||||
|
THCVaultConnectionInput,
|
||||||
|
TValidateHCVaultConnectionCredentialsSchema
|
||||||
|
} from "./hc-vault";
|
||||||
import {
|
import {
|
||||||
THumanitecConnection,
|
THumanitecConnection,
|
||||||
THumanitecConnectionConfig,
|
THumanitecConnectionConfig,
|
||||||
@@ -116,6 +122,7 @@ export type TAppConnection = { id: string } & (
|
|||||||
| TAzureClientSecretsConnection
|
| TAzureClientSecretsConnection
|
||||||
| TWindmillConnection
|
| TWindmillConnection
|
||||||
| TAuth0Connection
|
| TAuth0Connection
|
||||||
|
| THCVaultConnection
|
||||||
| TLdapConnection
|
| TLdapConnection
|
||||||
| TTeamCityConnection
|
| TTeamCityConnection
|
||||||
);
|
);
|
||||||
@@ -140,6 +147,7 @@ export type TAppConnectionInput = { id: string } & (
|
|||||||
| TAzureClientSecretsConnectionInput
|
| TAzureClientSecretsConnectionInput
|
||||||
| TWindmillConnectionInput
|
| TWindmillConnectionInput
|
||||||
| TAuth0ConnectionInput
|
| TAuth0ConnectionInput
|
||||||
|
| THCVaultConnectionInput
|
||||||
| TLdapConnectionInput
|
| TLdapConnectionInput
|
||||||
| TTeamCityConnectionInput
|
| TTeamCityConnectionInput
|
||||||
);
|
);
|
||||||
@@ -170,6 +178,7 @@ export type TAppConnectionConfig =
|
|||||||
| TVercelConnectionConfig
|
| TVercelConnectionConfig
|
||||||
| TWindmillConnectionConfig
|
| TWindmillConnectionConfig
|
||||||
| TAuth0ConnectionConfig
|
| TAuth0ConnectionConfig
|
||||||
|
| THCVaultConnectionConfig
|
||||||
| TLdapConnectionConfig
|
| TLdapConnectionConfig
|
||||||
| TTeamCityConnectionConfig;
|
| TTeamCityConnectionConfig;
|
||||||
|
|
||||||
@@ -189,6 +198,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
|||||||
| TValidateTerraformCloudConnectionCredentialsSchema
|
| TValidateTerraformCloudConnectionCredentialsSchema
|
||||||
| TValidateWindmillConnectionCredentialsSchema
|
| TValidateWindmillConnectionCredentialsSchema
|
||||||
| TValidateAuth0ConnectionCredentialsSchema
|
| TValidateAuth0ConnectionCredentialsSchema
|
||||||
|
| TValidateHCVaultConnectionCredentialsSchema
|
||||||
| TValidateLdapConnectionCredentialsSchema
|
| TValidateLdapConnectionCredentialsSchema
|
||||||
| TValidateTeamCityConnectionCredentialsSchema;
|
| TValidateTeamCityConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum HCVaultConnectionMethod {
|
||||||
|
AccessToken = "access-token",
|
||||||
|
AppRole = "app-role"
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
import { HCVaultConnectionMethod } from "./hc-vault-connection-enums";
|
||||||
|
import {
|
||||||
|
THCVaultConnection,
|
||||||
|
THCVaultConnectionConfig,
|
||||||
|
THCVaultMountResponse,
|
||||||
|
TValidateHCVaultConnectionCredentials
|
||||||
|
} from "./hc-vault-connection-types";
|
||||||
|
|
||||||
|
export const getHCVaultInstanceUrl = async (config: THCVaultConnectionConfig) => {
|
||||||
|
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
|
||||||
|
|
||||||
|
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||||
|
|
||||||
|
return instanceUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHCVaultConnectionListItem = () => ({
|
||||||
|
name: "HCVault" as const,
|
||||||
|
app: AppConnection.HCVault as const,
|
||||||
|
methods: Object.values(HCVaultConnectionMethod) as [
|
||||||
|
HCVaultConnectionMethod.AccessToken,
|
||||||
|
HCVaultConnectionMethod.AppRole
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
type TokenRespData = {
|
||||||
|
auth: {
|
||||||
|
client_token: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHCVaultAccessToken = async (connection: TValidateHCVaultConnectionCredentials) => {
|
||||||
|
// Return access token directly if not using AppRole method
|
||||||
|
if (connection.method !== HCVaultConnectionMethod.AppRole) {
|
||||||
|
return connection.credentials.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate temporary token for AppRole method
|
||||||
|
try {
|
||||||
|
const { instanceUrl, roleId, secretId } = connection.credentials;
|
||||||
|
const tokenResp = await request.post<TokenRespData>(
|
||||||
|
`${removeTrailingSlash(instanceUrl)}/v1/auth/approle/login`,
|
||||||
|
{ role_id: roleId, secret_id: secretId },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tokenResp.status !== 200) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate credentials: Hashicorp Vault responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResp.data.auth.client_token;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to validate connection: verify credentials"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateHCVaultConnectionCredentials = async (config: THCVaultConnectionConfig) => {
|
||||||
|
const instanceUrl = await getHCVaultInstanceUrl(config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const accessToken = await getHCVaultAccessToken(config);
|
||||||
|
|
||||||
|
// Verify token
|
||||||
|
await request.get(`${instanceUrl}/v1/auth/token/lookup-self`, {
|
||||||
|
headers: { "X-Vault-Token": accessToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
return config.credentials;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to validate connection: verify credentials"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listHCVaultMounts = async (appConnection: THCVaultConnection) => {
|
||||||
|
const instanceUrl = await getHCVaultInstanceUrl(appConnection);
|
||||||
|
const accessToken = await getHCVaultAccessToken(appConnection);
|
||||||
|
|
||||||
|
const { data } = await request.get<THCVaultMountResponse>(`${instanceUrl}/v1/sys/mounts`, {
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
...(appConnection.credentials.namespace ? { "X-Vault-Namespace": appConnection.credentials.namespace } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mounts: string[] = [];
|
||||||
|
|
||||||
|
// Filter for "kv" version 2 type only
|
||||||
|
Object.entries(data.data).forEach(([path, mount]) => {
|
||||||
|
if (mount.type === "kv" && mount.options?.version === "2") {
|
||||||
|
mounts.push(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mounts;
|
||||||
|
};
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
BaseAppConnectionSchema,
|
||||||
|
GenericCreateAppConnectionFieldsSchema,
|
||||||
|
GenericUpdateAppConnectionFieldsSchema
|
||||||
|
} from "@app/services/app-connection/app-connection-schemas";
|
||||||
|
|
||||||
|
import { HCVaultConnectionMethod } from "./hc-vault-connection-enums";
|
||||||
|
|
||||||
|
const InstanceUrlSchema = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Instance URL required")
|
||||||
|
.url("Invalid Instance URL")
|
||||||
|
.describe(AppConnections.CREDENTIALS.HC_VAULT.instanceUrl);
|
||||||
|
|
||||||
|
const NamespaceSchema = z.string().trim().optional().describe(AppConnections.CREDENTIALS.HC_VAULT.namespace);
|
||||||
|
|
||||||
|
export const HCVaultConnectionAccessTokenCredentialsSchema = z.object({
|
||||||
|
instanceUrl: InstanceUrlSchema,
|
||||||
|
namespace: NamespaceSchema,
|
||||||
|
accessToken: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Access Token required")
|
||||||
|
.describe(AppConnections.CREDENTIALS.HC_VAULT.accessToken)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const HCVaultConnectionAppRoleCredentialsSchema = z.object({
|
||||||
|
instanceUrl: InstanceUrlSchema,
|
||||||
|
namespace: NamespaceSchema,
|
||||||
|
roleId: z.string().trim().min(1, "Role ID required").describe(AppConnections.CREDENTIALS.HC_VAULT.roleId),
|
||||||
|
secretId: z.string().trim().min(1, "Secret ID required").describe(AppConnections.CREDENTIALS.HC_VAULT.secretId)
|
||||||
|
});
|
||||||
|
|
||||||
|
const BaseHCVaultConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.HCVault) });
|
||||||
|
|
||||||
|
export const HCVaultConnectionSchema = z.intersection(
|
||||||
|
BaseHCVaultConnectionSchema,
|
||||||
|
z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(HCVaultConnectionMethod.AccessToken),
|
||||||
|
credentials: HCVaultConnectionAccessTokenCredentialsSchema
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(HCVaultConnectionMethod.AppRole),
|
||||||
|
credentials: HCVaultConnectionAppRoleCredentialsSchema
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SanitizedHCVaultConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseHCVaultConnectionSchema.extend({
|
||||||
|
method: z.literal(HCVaultConnectionMethod.AccessToken),
|
||||||
|
credentials: HCVaultConnectionAccessTokenCredentialsSchema.pick({})
|
||||||
|
}),
|
||||||
|
BaseHCVaultConnectionSchema.extend({
|
||||||
|
method: z.literal(HCVaultConnectionMethod.AppRole),
|
||||||
|
credentials: HCVaultConnectionAppRoleCredentialsSchema.pick({})
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ValidateHCVaultConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z
|
||||||
|
.literal(HCVaultConnectionMethod.AccessToken)
|
||||||
|
.describe(AppConnections.CREATE(AppConnection.HCVault).method),
|
||||||
|
credentials: HCVaultConnectionAccessTokenCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.HCVault).credentials
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(HCVaultConnectionMethod.AppRole).describe(AppConnections.CREATE(AppConnection.HCVault).method),
|
||||||
|
credentials: HCVaultConnectionAppRoleCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.HCVault).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateHCVaultConnectionSchema = ValidateHCVaultConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.HCVault)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateHCVaultConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: z
|
||||||
|
.union([HCVaultConnectionAccessTokenCredentialsSchema, HCVaultConnectionAppRoleCredentialsSchema])
|
||||||
|
.optional()
|
||||||
|
.describe(AppConnections.UPDATE(AppConnection.HCVault).credentials)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.HCVault));
|
||||||
|
|
||||||
|
export const HCVaultConnectionListItemSchema = z.object({
|
||||||
|
name: z.literal("HCVault"),
|
||||||
|
app: z.literal(AppConnection.HCVault),
|
||||||
|
methods: z.nativeEnum(HCVaultConnectionMethod).array()
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import { listHCVaultMounts } from "./hc-vault-connection-fns";
|
||||||
|
import { THCVaultConnection } from "./hc-vault-connection-types";
|
||||||
|
|
||||||
|
type TGetAppConnectionFunc = (
|
||||||
|
app: AppConnection,
|
||||||
|
connectionId: string,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => Promise<THCVaultConnection>;
|
||||||
|
|
||||||
|
export const hcVaultConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||||
|
const listMounts = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.HCVault, connectionId, actor);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mounts = await listHCVaultMounts(appConnection);
|
||||||
|
return mounts;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Failed to establish connection with Hashicorp Vault");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listMounts
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateHCVaultConnectionSchema,
|
||||||
|
HCVaultConnectionSchema,
|
||||||
|
ValidateHCVaultConnectionCredentialsSchema
|
||||||
|
} from "./hc-vault-connection-schemas";
|
||||||
|
|
||||||
|
export type THCVaultConnection = z.infer<typeof HCVaultConnectionSchema>;
|
||||||
|
|
||||||
|
export type THCVaultConnectionInput = z.infer<typeof CreateHCVaultConnectionSchema> & {
|
||||||
|
app: AppConnection.HCVault;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateHCVaultConnectionCredentialsSchema = typeof ValidateHCVaultConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TValidateHCVaultConnectionCredentials = z.infer<typeof ValidateHCVaultConnectionCredentialsSchema>;
|
||||||
|
|
||||||
|
export type THCVaultConnectionConfig = DiscriminativePick<THCVaultConnectionInput, "method" | "app" | "credentials"> & {
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type THCVaultMountResponse = {
|
||||||
|
data: {
|
||||||
|
[key: string]: {
|
||||||
|
options: {
|
||||||
|
version?: string | null;
|
||||||
|
} | null;
|
||||||
|
type: string; // We're only interested in "kv" types
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./hc-vault-connection-enums";
|
||||||
|
export * from "./hc-vault-connection-fns";
|
||||||
|
export * from "./hc-vault-connection-schemas";
|
||||||
|
export * from "./hc-vault-connection-types";
|
||||||
@@ -2,7 +2,7 @@ import bcrypt from "bcrypt";
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas";
|
import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||||
@@ -20,6 +20,8 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
|||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenType } from "../auth-token/auth-token-types";
|
import { TokenType } from "../auth-token/auth-token-types";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
|
import { getDefaultOrgMembershipRole } from "../org/org-role-fns";
|
||||||
|
import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { LoginMethod } from "../super-admin/super-admin-types";
|
import { LoginMethod } from "../super-admin/super-admin-types";
|
||||||
import { TTotpServiceFactory } from "../totp/totp-service";
|
import { TTotpServiceFactory } from "../totp/totp-service";
|
||||||
@@ -48,6 +50,7 @@ type TAuthLoginServiceFactoryDep = {
|
|||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
||||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
|
orgMembershipDAL: TOrgMembershipDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||||
@@ -56,6 +59,7 @@ export const authLoginServiceFactory = ({
|
|||||||
tokenService,
|
tokenService,
|
||||||
smtpService,
|
smtpService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
|
orgMembershipDAL,
|
||||||
totpService,
|
totpService,
|
||||||
auditLogService
|
auditLogService
|
||||||
}: TAuthLoginServiceFactoryDep) => {
|
}: TAuthLoginServiceFactoryDep) => {
|
||||||
@@ -397,8 +401,8 @@ export const authLoginServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
|
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
|
||||||
const orgMfaMethod = selectedOrg.enforceMfa ? selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL : undefined;
|
const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||||
const userMfaMethod = user.isMfaEnabled ? user.selectedMfaMethod ?? MfaMethod.EMAIL : undefined;
|
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||||
const mfaMethod = orgMfaMethod ?? userMfaMethod;
|
const mfaMethod = orgMfaMethod ?? userMfaMethod;
|
||||||
|
|
||||||
if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) {
|
if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) {
|
||||||
@@ -569,9 +573,9 @@ export const authLoginServiceFactory = ({
|
|||||||
}: TVerifyMfaTokenDTO) => {
|
}: TVerifyMfaTokenDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const user = await userDAL.findById(userId);
|
const user = await userDAL.findById(userId);
|
||||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||||
if (mfaMethod === MfaMethod.EMAIL) {
|
if (mfaMethod === MfaMethod.EMAIL) {
|
||||||
await tokenService.validateTokenForUser({
|
await tokenService.validateTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_MFA,
|
type: TokenType.TOKEN_EMAIL_MFA,
|
||||||
@@ -719,6 +723,35 @@ export const authLoginServiceFactory = ({
|
|||||||
authMethods: [authMethod],
|
authMethods: [authMethod],
|
||||||
isGhost: false
|
isGhost: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (authMethod === AuthMethod.GITHUB && serverCfg.defaultAuthOrgId && !appCfg.isCloud) {
|
||||||
|
let orgId = "";
|
||||||
|
const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId);
|
||||||
|
if (!defaultOrg) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to find default organization with ID ${serverCfg.defaultAuthOrgId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
orgId = defaultOrg.id;
|
||||||
|
const [orgMembership] = await orgDAL.findMembership({
|
||||||
|
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
|
||||||
|
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!orgMembership) {
|
||||||
|
const { role, roleId } = await getDefaultOrgMembershipRole(defaultOrg.defaultMembershipRole);
|
||||||
|
|
||||||
|
await orgMembershipDAL.create({
|
||||||
|
userId: user.id,
|
||||||
|
inviteEmail: email,
|
||||||
|
orgId,
|
||||||
|
role,
|
||||||
|
roleId,
|
||||||
|
status: OrgMembershipStatus.Accepted,
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
||||||
if (isLinkingRequired) {
|
if (isLinkingRequired) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { getMinExpiresIn } from "@app/lib/fn";
|
import { getMinExpiresIn } from "@app/lib/fn";
|
||||||
import { isDisposableEmail } from "@app/lib/validator";
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
@@ -150,7 +150,8 @@ export const authSignupServiceFactory = ({
|
|||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
ip,
|
ip,
|
||||||
userAgent,
|
userAgent,
|
||||||
authorization
|
authorization,
|
||||||
|
useDefaultOrg
|
||||||
}: TCompleteAccountSignupDTO) => {
|
}: TCompleteAccountSignupDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
@@ -293,15 +294,24 @@ export const authSignupServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
const newOrganization = await orgService.createOrganization({
|
let orgId = "";
|
||||||
userId: user.id,
|
if (useDefaultOrg && serverCfg.defaultAuthOrgId && !appCfg.isCloud) {
|
||||||
userEmail: user.email ?? user.username,
|
const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId);
|
||||||
orgName: organizationName
|
if (!defaultOrg) throw new BadRequestError({ message: "Failed to find default organization" });
|
||||||
});
|
orgId = defaultOrg.id;
|
||||||
|
} else {
|
||||||
|
if (!organizationName) throw new BadRequestError({ message: "Organization name is required" });
|
||||||
|
const newOrganization = await orgService.createOrganization({
|
||||||
|
userId: user.id,
|
||||||
|
userEmail: user.email ?? user.username,
|
||||||
|
orgName: organizationName
|
||||||
|
});
|
||||||
|
|
||||||
if (!newOrganization) throw new Error("Failed to create organization");
|
if (!newOrganization) throw new Error("Failed to create organization");
|
||||||
|
orgId = newOrganization.id;
|
||||||
|
}
|
||||||
|
|
||||||
organizationId = newOrganization.id;
|
organizationId = orgId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedMembersips = await orgDAL.updateMembership(
|
const updatedMembersips = await orgDAL.updateMembership(
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ export type TCompleteAccountSignupDTO = {
|
|||||||
encryptedPrivateKeyTag: string;
|
encryptedPrivateKeyTag: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
organizationName: string;
|
organizationName?: string;
|
||||||
providerAuthToken?: string | null;
|
providerAuthToken?: string | null;
|
||||||
attributionSource?: string | undefined;
|
attributionSource?: string | undefined;
|
||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
authorization: string;
|
authorization: string;
|
||||||
|
useDefaultOrg?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCompleteAccountInviteDTO = {
|
export type TCompleteAccountInviteDTO = {
|
||||||
|
|||||||
@@ -0,0 +1,706 @@
|
|||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
|
import axios from "axios";
|
||||||
|
import { TeamsActivityHandler, TurnContext } from "botbuilder";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||||
|
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
|
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
||||||
|
import { WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||||
|
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
||||||
|
|
||||||
|
const ConsentError = "AADSTS65001";
|
||||||
|
|
||||||
|
export const verifyTenantFromCode = async (
|
||||||
|
tenantId: string,
|
||||||
|
code: string,
|
||||||
|
redirectUri: string,
|
||||||
|
clientId: string,
|
||||||
|
clientSecret: string
|
||||||
|
) => {
|
||||||
|
const getAccessToken = async (params: URLSearchParams) => {
|
||||||
|
const response = await axios
|
||||||
|
.post<{ access_token: string }>(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, params, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
if ((err.response?.data as { error_description?: string })?.error_description?.includes(ConsentError)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to verify tenant, please ensure that you have granted admin consent."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
logger.error(err.response?.data, "Error fetching Microsoft Teams access token");
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.access_token;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Azure App-based auth
|
||||||
|
const applicationAccessToken = await getAccessToken(
|
||||||
|
new URLSearchParams({
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
scope: "https://graph.microsoft.com/.default",
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
grant_type: "client_credentials"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// User-based auth
|
||||||
|
const authorizationAccessToken = await getAccessToken(
|
||||||
|
new URLSearchParams({
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
scope: "https://graph.microsoft.com/.default",
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify application token
|
||||||
|
const { tid: tenantIdFromApplicationAccessToken } = jwt.decode(applicationAccessToken) as { tid: string };
|
||||||
|
|
||||||
|
if (tenantIdFromApplicationAccessToken !== tenantId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid application token tenant ID. Expected ${tenantId}, got ${tenantIdFromApplicationAccessToken}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user authorization token
|
||||||
|
const { tid: tenantIdFromAuthorizationAccessToken } = jwt.decode(authorizationAccessToken) as { tid: string };
|
||||||
|
|
||||||
|
if (tenantIdFromAuthorizationAccessToken !== tenantId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid authorization token tenant ID. Expected ${tenantId}, got ${tenantIdFromAuthorizationAccessToken}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMicrosoftTeamsAccessToken = async (
|
||||||
|
{
|
||||||
|
orgId,
|
||||||
|
microsoftTeamsIntegrationId,
|
||||||
|
tenantId,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
getBotFrameworkToken
|
||||||
|
}: {
|
||||||
|
microsoftTeamsIntegrationId: string;
|
||||||
|
orgId: string;
|
||||||
|
tenantId: string;
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne" | "update">;
|
||||||
|
getBotFrameworkToken?: boolean;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const details = getBotFrameworkToken
|
||||||
|
? {
|
||||||
|
uri: "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token",
|
||||||
|
scope: "https://api.botframework.com/.default"
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
uri: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
||||||
|
scope: "https://graph.microsoft.com/.default"
|
||||||
|
};
|
||||||
|
|
||||||
|
const integration = await microsoftTeamsIntegrationDAL.findOne(
|
||||||
|
{
|
||||||
|
id: microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!integration) {
|
||||||
|
throw new BadRequestError({ message: "Microsoft Teams integration not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getBotFrameworkToken) {
|
||||||
|
// If the token expires within the next 5 minutes, we'll get a new token instead of using the stored one.
|
||||||
|
const currentTime = new Date(new Date().getTime() + 5 * 60 * 1000);
|
||||||
|
|
||||||
|
if (
|
||||||
|
integration.encryptedBotAccessToken &&
|
||||||
|
integration.botAccessTokenExpiresAt &&
|
||||||
|
integration.botAccessTokenExpiresAt > currentTime
|
||||||
|
) {
|
||||||
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
orgId,
|
||||||
|
type: KmsDataKey.Organization
|
||||||
|
});
|
||||||
|
|
||||||
|
const botAccessToken = decryptor({
|
||||||
|
cipherTextBlob: integration.encryptedBotAccessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
return botAccessToken.toString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the token expires within the next 5 minutes, we'll get a new token instead of using the stored one.
|
||||||
|
const currentTime = new Date(new Date().getTime() + 5 * 60 * 1000);
|
||||||
|
|
||||||
|
if (
|
||||||
|
integration.encryptedAccessToken &&
|
||||||
|
integration.accessTokenExpiresAt &&
|
||||||
|
integration.accessTokenExpiresAt > currentTime
|
||||||
|
) {
|
||||||
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
orgId,
|
||||||
|
type: KmsDataKey.Organization
|
||||||
|
});
|
||||||
|
|
||||||
|
const accessToken = decryptor({
|
||||||
|
cipherTextBlob: integration.encryptedAccessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
return accessToken.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenResponse = await axios.post<{ access_token: string; expires_in: number }>(
|
||||||
|
details.uri,
|
||||||
|
new URLSearchParams({
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
scope: details.scope,
|
||||||
|
grant_type: "client_credentials"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (getBotFrameworkToken) {
|
||||||
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
orgId,
|
||||||
|
type: KmsDataKey.Organization
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedBotAccessToken } = encryptor({
|
||||||
|
plainText: Buffer.from(tokenResponse.data.access_token)
|
||||||
|
});
|
||||||
|
|
||||||
|
const expiresAt = new Date(new Date().getTime() + tokenResponse.data.expires_in * 1000);
|
||||||
|
|
||||||
|
await microsoftTeamsIntegrationDAL.update(
|
||||||
|
{
|
||||||
|
id: microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
botAccessTokenExpiresAt: expiresAt,
|
||||||
|
encryptedBotAccessToken
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
orgId,
|
||||||
|
type: KmsDataKey.Organization
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedAccessToken } = encryptor({
|
||||||
|
plainText: Buffer.from(tokenResponse.data.access_token)
|
||||||
|
});
|
||||||
|
|
||||||
|
const expiresAt = new Date(new Date().getTime() + tokenResponse.data.expires_in * 1000);
|
||||||
|
|
||||||
|
await microsoftTeamsIntegrationDAL.update(
|
||||||
|
{
|
||||||
|
id: microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessTokenExpiresAt: expiresAt,
|
||||||
|
encryptedAccessToken
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse.data.access_token;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
logger.error(
|
||||||
|
error.response?.data,
|
||||||
|
`getMicrosoftTeamsAccessToken: Error fetching Microsoft Teams access token [status-code=${error.response?.status}]`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(error, "getMicrosoftTeamsAccessToken: Error fetching Microsoft Teams access token");
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isBotInstalledInTenant = async (
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
botAppId,
|
||||||
|
botAppPassword,
|
||||||
|
botId,
|
||||||
|
orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId
|
||||||
|
}: {
|
||||||
|
tenantId: string;
|
||||||
|
botAppId: string;
|
||||||
|
botAppPassword: string;
|
||||||
|
botId: string;
|
||||||
|
orgId: string;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne" | "update">;
|
||||||
|
microsoftTeamsIntegrationId: string;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const botAccessToken = await getMicrosoftTeamsAccessToken(
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
clientId: botAppId.toString(),
|
||||||
|
clientSecret: botAppPassword.toString(),
|
||||||
|
getBotFrameworkToken: true,
|
||||||
|
orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
).catch(() => null);
|
||||||
|
|
||||||
|
const accessToken = await getMicrosoftTeamsAccessToken(
|
||||||
|
{
|
||||||
|
orgId,
|
||||||
|
tenantId,
|
||||||
|
clientId: botAppId.toString(),
|
||||||
|
clientSecret: botAppPassword.toString(),
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
).catch(() => null);
|
||||||
|
|
||||||
|
if (!botAccessToken || !accessToken) {
|
||||||
|
return {
|
||||||
|
accessToken: null,
|
||||||
|
botAccessToken: null,
|
||||||
|
installed: false,
|
||||||
|
internalId: null
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appsResponse = await axios
|
||||||
|
.get<{ value: { id: string; displayName: string; distributionMethod: string; externalId: string }[] }>(
|
||||||
|
"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(error, "Error fetching installed apps");
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!appsResponse) {
|
||||||
|
return {
|
||||||
|
installed: false,
|
||||||
|
internalId: null,
|
||||||
|
accessToken,
|
||||||
|
botAccessToken
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const botInstalledInTenant = appsResponse.data.value.find((a) => a.externalId === botId);
|
||||||
|
|
||||||
|
if (!botInstalledInTenant) {
|
||||||
|
return {
|
||||||
|
installed: false,
|
||||||
|
internalId: null,
|
||||||
|
accessToken,
|
||||||
|
botAccessToken
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
installed: true,
|
||||||
|
internalId: botInstalledInTenant.id,
|
||||||
|
accessToken,
|
||||||
|
botAccessToken
|
||||||
|
} as const;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Error fetching installed apps");
|
||||||
|
return {
|
||||||
|
installed: false,
|
||||||
|
internalId: null,
|
||||||
|
accessToken: null,
|
||||||
|
botAccessToken: null
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildTeamsPayload = (notification: TNotification) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
switch (notification.type) {
|
||||||
|
case TriggerFeature.SECRET_APPROVAL: {
|
||||||
|
const { payload } = notification;
|
||||||
|
|
||||||
|
const adaptiveCard = {
|
||||||
|
type: "AdaptiveCard",
|
||||||
|
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||||
|
version: "1.5",
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: "TextBlock",
|
||||||
|
text: "Secret approval request",
|
||||||
|
weight: "Bolder",
|
||||||
|
size: "Large"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "TextBlock",
|
||||||
|
text: `A secret approval request has been opened by ${payload.userEmail}.`,
|
||||||
|
wrap: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "FactSet",
|
||||||
|
facts: [
|
||||||
|
{
|
||||||
|
title: "Environment",
|
||||||
|
value: payload.environment
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Secret path",
|
||||||
|
value: payload.secretPath || "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `Secret Key${payload.secretKeys.length > 1 ? "s" : ""}`,
|
||||||
|
value: payload.secretKeys.join(", ")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: "Action.OpenUrl",
|
||||||
|
title: "View request in Infisical",
|
||||||
|
url: `${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${payload.requestId}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
adaptiveCard
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case TriggerFeature.ACCESS_REQUEST: {
|
||||||
|
const { payload } = notification;
|
||||||
|
|
||||||
|
const adaptiveCard = {
|
||||||
|
type: "AdaptiveCard",
|
||||||
|
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||||
|
version: "1.5",
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: "TextBlock",
|
||||||
|
text: "New access approval request pending for review",
|
||||||
|
weight: "Bolder",
|
||||||
|
size: "Large"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "TextBlock",
|
||||||
|
text: `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
|
||||||
|
payload.isTemporary ? "temporary" : "permanent"
|
||||||
|
} access to path '${payload.secretPath}' in the ${payload.environment} environment of ${
|
||||||
|
payload.projectName
|
||||||
|
} project.`,
|
||||||
|
wrap: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "TextBlock",
|
||||||
|
text: `The following permissions are requested: ${payload.permissions.join(", ")}`,
|
||||||
|
wrap: true
|
||||||
|
},
|
||||||
|
payload.note
|
||||||
|
? {
|
||||||
|
type: "TextBlock",
|
||||||
|
text: `**User Note**: ${payload.note}`,
|
||||||
|
wrap: true
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
].filter(Boolean),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: "Action.OpenUrl",
|
||||||
|
title: "View request in Infisical",
|
||||||
|
url: payload.approvalUrl
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
adaptiveCard
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Teams notification type not supported."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class TeamsBot extends TeamsActivityHandler {
|
||||||
|
private botAppId: string;
|
||||||
|
|
||||||
|
private botAppPassword: string;
|
||||||
|
|
||||||
|
private workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "update">;
|
||||||
|
|
||||||
|
private microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne">;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
botAppId,
|
||||||
|
botAppPassword,
|
||||||
|
workflowIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationDAL
|
||||||
|
}: {
|
||||||
|
botAppId: string;
|
||||||
|
botAppPassword: string;
|
||||||
|
workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "update">;
|
||||||
|
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne">;
|
||||||
|
}) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.botAppId = botAppId;
|
||||||
|
this.botAppPassword = botAppPassword;
|
||||||
|
this.workflowIntegrationDAL = workflowIntegrationDAL;
|
||||||
|
this.microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDAL;
|
||||||
|
|
||||||
|
// We know when a bot is added, but we can't know when it's fully removed from the tenant.
|
||||||
|
this.onTeamsMembersAddedEvent(async (membersAdded, _, context) => {
|
||||||
|
const botWasAdded = membersAdded.some((member) => member.id === context.activity.recipient.id);
|
||||||
|
|
||||||
|
if (botWasAdded && context.activity.conversation.tenantId) {
|
||||||
|
const microsoftTeamIntegration = await this.microsoftTeamsIntegrationDAL
|
||||||
|
.findOne({
|
||||||
|
tenantId: context.activity.conversation.tenantId
|
||||||
|
})
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
if (microsoftTeamIntegration) {
|
||||||
|
await this.workflowIntegrationDAL
|
||||||
|
.update(
|
||||||
|
{
|
||||||
|
id: microsoftTeamIntegration.id,
|
||||||
|
status: WorkflowIntegrationStatus.PENDING
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: WorkflowIntegrationStatus.INSTALLED
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(error, "Microsoft Teams Workflow Integration: Failed to update workflow integration");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is required in order for the bot to send proactive messages, which is required for the bot to pass the bot release validation step.
|
||||||
|
await context.sendActivity(
|
||||||
|
"👋 Thanks for installing the Infisical app! You can now use the bot to send notifications to your selected teams."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(context: TurnContext) {
|
||||||
|
logger.info(context, "Processing Microsoft Teams context");
|
||||||
|
await super.run(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessageToChannel(
|
||||||
|
botAccessToken: string,
|
||||||
|
tenantId: string,
|
||||||
|
channelId: string,
|
||||||
|
teamId: string,
|
||||||
|
notification: TNotification
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { adaptiveCard } = buildTeamsPayload(notification);
|
||||||
|
|
||||||
|
const adaptiveCardActivity = {
|
||||||
|
type: "message",
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
contentType: "application/vnd.microsoft.card.adaptive",
|
||||||
|
content: adaptiveCard
|
||||||
|
}
|
||||||
|
],
|
||||||
|
conversation: {
|
||||||
|
id: channelId,
|
||||||
|
isGroup: true
|
||||||
|
},
|
||||||
|
channelData: {
|
||||||
|
channel: {
|
||||||
|
id: channelId
|
||||||
|
},
|
||||||
|
team: {
|
||||||
|
id: teamId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(
|
||||||
|
`https://smba.trafficmanager.net/amer/v3/conversations/${channelId}/activities`,
|
||||||
|
adaptiveCardActivity,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${botAccessToken}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
logger.error(
|
||||||
|
error.response?.data,
|
||||||
|
`sendMessageToChannel: Axios Error, Microsoft Teams Workflow Integration: Failed to send message to channel [channelId=${channelId}] [teamId=${teamId}] [tenantId=${tenantId}]`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
error,
|
||||||
|
`sendMessageToChannel: Microsoft Teams Workflow Integration: Failed to send message to channel [channelId=${channelId}] [teamId=${teamId}] [tenantId=${tenantId}]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamsAndChannels(accessToken: string, internalAppId: string) {
|
||||||
|
try {
|
||||||
|
let teamsNextLink: string = "https://graph.microsoft.com/v1.0/teams";
|
||||||
|
|
||||||
|
let allTeams: { displayName: string; id: string }[] = [];
|
||||||
|
while (teamsNextLink?.length) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const response = await axios.get<{
|
||||||
|
value: { displayName: string; id: string }[];
|
||||||
|
"@odata.nextLink"?: string;
|
||||||
|
}>(teamsNextLink, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
allTeams = allTeams.concat(response.data.value);
|
||||||
|
teamsNextLink = response.data["@odata.nextLink"] || "";
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Microsoft Teams Workflow Integration: Failed to fetch teams");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for await (const team of allTeams) {
|
||||||
|
try {
|
||||||
|
// Get installed apps for this team
|
||||||
|
const installedAppsResponse = await axios.get<{ value: { teamsAppDefinition: { teamsAppId: string } }[] }>(
|
||||||
|
`https://graph.microsoft.com/v1.0/teams/${team.id}/installedApps?$expand=teamsAppDefinition`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!installedAppsResponse.data.value.some((app) => app.teamsAppDefinition.teamsAppId === internalAppId)) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue; // skip this team if we can't determine if the bot is installed
|
||||||
|
}
|
||||||
|
|
||||||
|
let allChannels: { displayName: string; id: string }[] = [];
|
||||||
|
|
||||||
|
let channelNextLink: string = `https://graph.microsoft.com/v1.0/teams/${team.id}/channels`;
|
||||||
|
|
||||||
|
while (channelNextLink?.length) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const resp = await axios
|
||||||
|
.get<{
|
||||||
|
value: { displayName: string; id: string }[];
|
||||||
|
"@odata.nextLink"?: string;
|
||||||
|
}>(channelNextLink, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
logger.error(
|
||||||
|
error.response?.data,
|
||||||
|
"getTeamsAndChannels: Axios error, Microsoft Teams Workflow Integration: Failed to fetch channels"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
error,
|
||||||
|
"getTeamsAndChannels: Microsoft Teams Workflow Integration: Failed to fetch channels"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
allChannels = allChannels.concat(resp.data.value);
|
||||||
|
channelNextLink = resp.data["@odata.nextLink"] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const channels = allChannels.map((channel) => ({
|
||||||
|
channelName: channel.displayName,
|
||||||
|
channelId: channel.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
teamId: team.id,
|
||||||
|
teamName: team.displayName,
|
||||||
|
channels
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Microsoft Teams Workflow Integration: Error fetching teams and channels");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateMicrosoftTeamsChannelsSchema = z
|
||||||
|
.object({
|
||||||
|
teamId: z.string(),
|
||||||
|
channelIds: z.array(z.string()).min(1)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.refine((data) => data === undefined || data?.channelIds.length <= 20, {
|
||||||
|
message: "You can only select up to 20 Microsoft Teams channels"
|
||||||
|
});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName, TMicrosoftTeamsIntegrations, TWorkflowIntegrations } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TMicrosoftTeamsIntegrationDALFactory = ReturnType<typeof microsoftTeamsIntegrationDALFactory>;
|
||||||
|
|
||||||
|
export const microsoftTeamsIntegrationDALFactory = (db: TDbClient) => {
|
||||||
|
const microsoftTeamsIntegrationOrm = ormify(db, TableName.MicrosoftTeamsIntegrations);
|
||||||
|
|
||||||
|
const findByIdWithWorkflowIntegrationDetails = async (id: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
return await (tx || db.replicaNode())(TableName.MicrosoftTeamsIntegrations)
|
||||||
|
.join(
|
||||||
|
TableName.WorkflowIntegrations,
|
||||||
|
`${TableName.MicrosoftTeamsIntegrations}.id`,
|
||||||
|
`${TableName.WorkflowIntegrations}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.MicrosoftTeamsIntegrations))
|
||||||
|
.select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("description").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("integration").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("slug").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("status").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.where(`${TableName.WorkflowIntegrations}.id`, id)
|
||||||
|
.first();
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find by ID with Workflow integration details" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findWithWorkflowIntegrationDetails = async (
|
||||||
|
filter: Partial<TMicrosoftTeamsIntegrations> & Partial<TWorkflowIntegrations>,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return await (tx || db.replicaNode())(TableName.MicrosoftTeamsIntegrations)
|
||||||
|
.join(
|
||||||
|
TableName.WorkflowIntegrations,
|
||||||
|
`${TableName.MicrosoftTeamsIntegrations}.id`,
|
||||||
|
`${TableName.WorkflowIntegrations}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.MicrosoftTeamsIntegrations))
|
||||||
|
.select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("description").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("integration").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("slug").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("status").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.where(filter);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find with Workflow integration details" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...microsoftTeamsIntegrationOrm,
|
||||||
|
findByIdWithWorkflowIntegrationDetails,
|
||||||
|
findWithWorkflowIntegrationDetails
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,710 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import {
|
||||||
|
CloudAdapter,
|
||||||
|
ConfigurationBotFrameworkAuthentication,
|
||||||
|
ConfigurationServiceClientCredentialFactory,
|
||||||
|
Request,
|
||||||
|
Response
|
||||||
|
} from "botbuilder";
|
||||||
|
import { FastifyReply, FastifyRequest } from "fastify";
|
||||||
|
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
|
import { TSuperAdminDALFactory } from "../super-admin/super-admin-dal";
|
||||||
|
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
||||||
|
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||||
|
import {
|
||||||
|
getMicrosoftTeamsAccessToken,
|
||||||
|
isBotInstalledInTenant,
|
||||||
|
TeamsBot,
|
||||||
|
verifyTenantFromCode
|
||||||
|
} from "./microsoft-teams-fns";
|
||||||
|
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
||||||
|
import {
|
||||||
|
TCheckInstallationStatusDTO,
|
||||||
|
TCreateMicrosoftTeamsIntegrationDTO,
|
||||||
|
TDeleteMicrosoftTeamsIntegrationDTO,
|
||||||
|
TGetClientIdDTO,
|
||||||
|
TGetMicrosoftTeamsIntegrationByIdDTO,
|
||||||
|
TGetMicrosoftTeamsIntegrationByOrgDTO,
|
||||||
|
TGetTeamsDTO,
|
||||||
|
TSendNotificationDTO,
|
||||||
|
TUpdateMicrosoftTeamsIntegrationDTO
|
||||||
|
} from "./microsoft-teams-types";
|
||||||
|
|
||||||
|
function requestBodyToRecord(body: unknown): Record<string, unknown> {
|
||||||
|
// if body is null or undefined, return an empty object
|
||||||
|
if (body === null || body === undefined) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// if body is not an object or is an array, return an empty object
|
||||||
|
if (typeof body !== "object" || Array.isArray(body)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// at this point, we know body is an object, so safe to cast
|
||||||
|
return body as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TMicrosoftTeamsServiceFactoryDep = {
|
||||||
|
microsoftTeamsIntegrationDAL: Pick<
|
||||||
|
TMicrosoftTeamsIntegrationDALFactory,
|
||||||
|
| "deleteById"
|
||||||
|
| "updateById"
|
||||||
|
| "create"
|
||||||
|
| "findOne"
|
||||||
|
| "findById"
|
||||||
|
| "findByIdWithWorkflowIntegrationDetails"
|
||||||
|
| "findWithWorkflowIntegrationDetails"
|
||||||
|
| "update"
|
||||||
|
>;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithRootKey" | "decryptWithRootKey">;
|
||||||
|
workflowIntegrationDAL: Pick<
|
||||||
|
TWorkflowIntegrationDALFactory,
|
||||||
|
"transaction" | "create" | "updateById" | "deleteById" | "update" | "findOne"
|
||||||
|
>;
|
||||||
|
serverCfgDAL: Pick<TSuperAdminDALFactory, "findById">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TMicrosoftTeamsServiceFactory = ReturnType<typeof microsoftTeamsServiceFactory>;
|
||||||
|
|
||||||
|
const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
|
export const microsoftTeamsServiceFactory = ({
|
||||||
|
permissionService,
|
||||||
|
serverCfgDAL,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
workflowIntegrationDAL
|
||||||
|
}: TMicrosoftTeamsServiceFactoryDep) => {
|
||||||
|
let teamsBot: TeamsBot | null = null;
|
||||||
|
let adapter: CloudAdapter | null = null;
|
||||||
|
|
||||||
|
const initializeTeamsBot = async ({ botAppId, botAppPassword }: { botAppId: string; botAppPassword: string }) => {
|
||||||
|
logger.info("Initializing Microsoft Teams bot");
|
||||||
|
teamsBot = new TeamsBot({
|
||||||
|
botAppId,
|
||||||
|
botAppPassword,
|
||||||
|
workflowIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
adapter = new CloudAdapter(
|
||||||
|
new ConfigurationBotFrameworkAuthentication(
|
||||||
|
{},
|
||||||
|
new ConfigurationServiceClientCredentialFactory({
|
||||||
|
MicrosoftAppId: botAppId,
|
||||||
|
MicrosoftAppPassword: botAppPassword,
|
||||||
|
MicrosoftAppType: "MultiTenant"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = async () => {
|
||||||
|
try {
|
||||||
|
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||||
|
|
||||||
|
if (
|
||||||
|
serverCfg?.encryptedMicrosoftTeamsAppId &&
|
||||||
|
serverCfg?.encryptedMicrosoftTeamsClientSecret &&
|
||||||
|
serverCfg?.encryptedMicrosoftTeamsBotId
|
||||||
|
) {
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||||
|
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||||
|
|
||||||
|
await initializeTeamsBot({
|
||||||
|
botAppId: decryptedAppId.toString(),
|
||||||
|
botAppPassword: decryptedAppPassword.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, "Error initializing Microsoft Teams bot on startup");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkInstallationStatus = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
workflowIntegrationId
|
||||||
|
}: TCheckInstallationStatusDTO) => {
|
||||||
|
const microsoftTeamsIntegration =
|
||||||
|
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId);
|
||||||
|
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID ${workflowIntegrationId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
microsoftTeamsIntegration.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||||
|
if (!serverCfg) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to get server configuration."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsAppId ||
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsClientSecret ||
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsBotId
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||||
|
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||||
|
const decryptedBotId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsBotId);
|
||||||
|
|
||||||
|
const teamsBotInfo = await isBotInstalledInTenant({
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
|
botAppId: decryptedAppId.toString(),
|
||||||
|
botAppPassword: decryptedAppPassword.toString(),
|
||||||
|
botId: decryptedBotId.toString(),
|
||||||
|
orgId: microsoftTeamsIntegration.orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsIntegration.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!teamsBotInfo.installed) {
|
||||||
|
if (microsoftTeamsIntegration.status === WorkflowIntegrationStatus.INSTALLED) {
|
||||||
|
await workflowIntegrationDAL.updateById(microsoftTeamsIntegration.id, {
|
||||||
|
status: WorkflowIntegrationStatus.PENDING
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams bot is not installed in the configured Microsoft Teams Tenant"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (microsoftTeamsIntegration.status !== WorkflowIntegrationStatus.INSTALLED) {
|
||||||
|
await workflowIntegrationDAL.updateById(microsoftTeamsIntegration.id, {
|
||||||
|
status: WorkflowIntegrationStatus.INSTALLED
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return microsoftTeamsIntegration;
|
||||||
|
};
|
||||||
|
|
||||||
|
const completeMicrosoftTeamsIntegration = async ({
|
||||||
|
code,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
tenantId,
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
|
redirectUri
|
||||||
|
}: TCreateMicrosoftTeamsIntegrationDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||||
|
if (!serverCfg) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to get server configuration."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { encryptedMicrosoftTeamsAppId, encryptedMicrosoftTeamsClientSecret, encryptedMicrosoftTeamsBotId } =
|
||||||
|
serverCfg;
|
||||||
|
|
||||||
|
if (!encryptedMicrosoftTeamsAppId || !encryptedMicrosoftTeamsClientSecret || !encryptedMicrosoftTeamsBotId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const botAppId = decryptWithRoot(encryptedMicrosoftTeamsAppId);
|
||||||
|
const botAppPassword = decryptWithRoot(encryptedMicrosoftTeamsClientSecret);
|
||||||
|
const botId = decryptWithRoot(encryptedMicrosoftTeamsBotId);
|
||||||
|
|
||||||
|
await verifyTenantFromCode(tenantId, code, redirectUri, botAppId.toString(), botAppPassword.toString());
|
||||||
|
|
||||||
|
await workflowIntegrationDAL.transaction(async (tx) => {
|
||||||
|
const workflowIntegration = await workflowIntegrationDAL.create(
|
||||||
|
{
|
||||||
|
description,
|
||||||
|
orgId: actorOrgId,
|
||||||
|
slug,
|
||||||
|
integration: WorkflowIntegration.MICROSOFT_TEAMS,
|
||||||
|
status: WorkflowIntegrationStatus.PENDING
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL
|
||||||
|
.create(
|
||||||
|
{
|
||||||
|
// @ts-expect-error id is kept as fixed because it is always equal to the workflow integration ID
|
||||||
|
id: workflowIntegration.id,
|
||||||
|
tenantId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
if (err instanceof DatabaseError) {
|
||||||
|
if ((err.error as Error)?.stack?.includes("duplicate key value violates unique constraint"))
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams integration with the same Tenant ID already exists."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamsBotInfo = await isBotInstalledInTenant(
|
||||||
|
{
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
|
botAppId: botAppId.toString(),
|
||||||
|
botAppPassword: botAppPassword.toString(),
|
||||||
|
botId: botId.toString(),
|
||||||
|
orgId: workflowIntegration.orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsIntegration.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (teamsBotInfo.installed) {
|
||||||
|
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
orgId: workflowIntegration.orgId,
|
||||||
|
type: KmsDataKey.Organization
|
||||||
|
});
|
||||||
|
const { cipherTextBlob: encryptedAccessToken } = orgDataKeyEncryptor({
|
||||||
|
plainText: Buffer.from(teamsBotInfo.accessToken, "utf8")
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({
|
||||||
|
plainText: Buffer.from(teamsBotInfo.botAccessToken, "utf8")
|
||||||
|
});
|
||||||
|
await microsoftTeamsIntegrationDAL.updateById(
|
||||||
|
microsoftTeamsIntegration.id,
|
||||||
|
{
|
||||||
|
internalTeamsAppId: teamsBotInfo.internalId,
|
||||||
|
encryptedAccessToken,
|
||||||
|
encryptedBotAccessToken
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await workflowIntegrationDAL.updateById(
|
||||||
|
workflowIntegration.id,
|
||||||
|
{
|
||||||
|
status: WorkflowIntegrationStatus.INSTALLED
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const getClientId = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGetClientIdDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||||
|
if (!serverCfg) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to get server configuration."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serverCfg.encryptedMicrosoftTeamsAppId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams app ID is not set"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const clientId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||||
|
|
||||||
|
return clientId.toString();
|
||||||
|
};
|
||||||
|
const getMicrosoftTeamsIntegrationsByOrg = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
}: TGetMicrosoftTeamsIntegrationByOrgDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const microsoftTeamsIntegrations = await microsoftTeamsIntegrationDAL.findWithWorkflowIntegrationDetails({
|
||||||
|
orgId: actorOrgId,
|
||||||
|
status: WorkflowIntegrationStatus.INSTALLED
|
||||||
|
});
|
||||||
|
|
||||||
|
return microsoftTeamsIntegrations.map((integration) => ({
|
||||||
|
...integration,
|
||||||
|
status: integration.status as WorkflowIntegrationStatus,
|
||||||
|
tenantId: integration.tenantId
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMicrosoftTeamsIntegrationById = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
id
|
||||||
|
}: TGetMicrosoftTeamsIntegrationByIdDTO) => {
|
||||||
|
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Microsoft Teams integration not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
microsoftTeamsIntegration.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...microsoftTeamsIntegration,
|
||||||
|
status: microsoftTeamsIntegration.status as WorkflowIntegrationStatus
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMicrosoftTeamsIntegration = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
id,
|
||||||
|
slug,
|
||||||
|
description
|
||||||
|
}: TUpdateMicrosoftTeamsIntegrationDTO) => {
|
||||||
|
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID ${id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
microsoftTeamsIntegration.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const updatedIntegration = await workflowIntegrationDAL.transaction(async (tx) => {
|
||||||
|
await workflowIntegrationDAL.updateById(
|
||||||
|
microsoftTeamsIntegration.id,
|
||||||
|
{
|
||||||
|
slug,
|
||||||
|
description
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const integration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(
|
||||||
|
microsoftTeamsIntegration.id,
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!integration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID ${microsoftTeamsIntegration.id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...integration,
|
||||||
|
status: integration.status as WorkflowIntegrationStatus
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedIntegration;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMicrosoftTeamsIntegration = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
id
|
||||||
|
}: TDeleteMicrosoftTeamsIntegrationDTO) => {
|
||||||
|
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID ${id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
microsoftTeamsIntegration.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
await workflowIntegrationDAL.deleteById(id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...microsoftTeamsIntegration,
|
||||||
|
status: microsoftTeamsIntegration.status as WorkflowIntegrationStatus
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTeams = async ({ actorId, actor, actorOrgId, actorAuthMethod, workflowIntegrationId }: TGetTeamsDTO) => {
|
||||||
|
const microsoftTeamsIntegration =
|
||||||
|
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId);
|
||||||
|
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID ${workflowIntegrationId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
microsoftTeamsIntegration.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
if (!teamsBot || !adapter) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to get teams and channels because the Microsoft Teams bot is uninitialized"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||||
|
if (!serverCfg) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to get server configuration."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsAppId ||
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsClientSecret ||
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsBotId
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||||
|
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||||
|
const decryptedBotId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsBotId);
|
||||||
|
|
||||||
|
const { installed, internalId, accessToken } = await isBotInstalledInTenant({
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
|
botAppId: decryptedAppId.toString(),
|
||||||
|
botAppPassword: decryptedAppPassword.toString(),
|
||||||
|
botId: decryptedBotId.toString(),
|
||||||
|
orgId: microsoftTeamsIntegration.orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsIntegration.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!installed) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams bot is not installed in the configured Microsoft Teams Tenant"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const teams = await teamsBot.getTeamsAndChannels(accessToken, internalId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...microsoftTeamsIntegration,
|
||||||
|
teams
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMessageEndpoint = async (req: FastifyRequest, res: FastifyReply) => {
|
||||||
|
if (!teamsBot || !adapter) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to handle message endpoint because the Microsoft Teams bot is uninitialized"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to manually build a Response object because the BotFrameworkAdapter expects a Response object. We are using FastifyReply as the underlying socket.
|
||||||
|
const response: Response = {
|
||||||
|
socket: res.raw.socket,
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
end(...args: any[]): unknown {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
res.raw.end(...args);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
header(name: string, value: unknown): unknown {
|
||||||
|
res.raw.setHeader(name, value as string);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
send(...args: unknown[]): unknown {
|
||||||
|
// For the first argument, which is typically the body
|
||||||
|
if (args.length > 0) {
|
||||||
|
const body = args[0];
|
||||||
|
|
||||||
|
if (typeof body === "string" || Buffer.isBuffer(body)) {
|
||||||
|
res.raw.write(body);
|
||||||
|
} else if (body !== null && body !== undefined) {
|
||||||
|
const json = JSON.stringify(body);
|
||||||
|
if (!res.raw.headersSent && !res.raw.getHeader("content-type")) {
|
||||||
|
res.raw.setHeader("content-type", "application/json");
|
||||||
|
}
|
||||||
|
res.raw.write(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastArg = args[args.length - 1];
|
||||||
|
if (typeof lastArg === "function") {
|
||||||
|
lastArg();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
status(code: number): unknown {
|
||||||
|
res.raw.statusCode = code;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const request: Request = {
|
||||||
|
body: requestBodyToRecord(req.body),
|
||||||
|
headers: req.headers,
|
||||||
|
method: req.method
|
||||||
|
};
|
||||||
|
|
||||||
|
await adapter.process(request, response, async (context) => {
|
||||||
|
await teamsBot?.run(context);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendNotification = async ({
|
||||||
|
tenantId,
|
||||||
|
target,
|
||||||
|
notification,
|
||||||
|
orgId,
|
||||||
|
microsoftTeamsIntegrationId
|
||||||
|
}: TSendNotificationDTO) => {
|
||||||
|
if (!teamsBot || !adapter) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to send notification because the Microsoft Teams bot is uninitialized"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||||
|
if (!serverCfg) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to get server configuration."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsAppId ||
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsClientSecret ||
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsBotId
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const botAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||||
|
const botAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||||
|
|
||||||
|
const botAccessToken = await getMicrosoftTeamsAccessToken({
|
||||||
|
tenantId,
|
||||||
|
clientId: botAppId.toString(),
|
||||||
|
clientSecret: botAppPassword.toString(),
|
||||||
|
getBotFrameworkToken: true,
|
||||||
|
orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const channelId of target.channelIds) {
|
||||||
|
await teamsBot.sendMessageToChannel(botAccessToken, tenantId, channelId, target.teamId, notification);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getMicrosoftTeamsIntegrationsByOrg,
|
||||||
|
getMicrosoftTeamsIntegrationById,
|
||||||
|
updateMicrosoftTeamsIntegration,
|
||||||
|
deleteMicrosoftTeamsIntegration,
|
||||||
|
completeMicrosoftTeamsIntegration,
|
||||||
|
initializeTeamsBot,
|
||||||
|
getTeams,
|
||||||
|
handleMessageEndpoint,
|
||||||
|
start,
|
||||||
|
sendNotification,
|
||||||
|
checkInstallationStatus,
|
||||||
|
getClientId
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
import { TNotification } from "@app/lib/workflow-integrations/types";
|
||||||
|
|
||||||
|
export type TGetMicrosoftTeamsIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TGetClientIdDTO = Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TCreateMicrosoftTeamsIntegrationDTO = Omit<TOrgPermission, "orgId"> & {
|
||||||
|
tenantId: string;
|
||||||
|
slug: string;
|
||||||
|
redirectUri: string;
|
||||||
|
description?: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCheckInstallationStatusDTO = { workflowIntegrationId: string } & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TGetMicrosoftTeamsIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TUpdateMicrosoftTeamsIntegrationDTO = { id: string; slug?: string; description?: string } & Omit<
|
||||||
|
TOrgPermission,
|
||||||
|
"orgId"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type TGetTeamsDTO = Omit<TOrgPermission, "orgId"> & {
|
||||||
|
workflowIntegrationId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteMicrosoftTeamsIntegrationDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TSendNotificationDTO = {
|
||||||
|
tenantId: string;
|
||||||
|
microsoftTeamsIntegrationId: string;
|
||||||
|
orgId: string;
|
||||||
|
target: {
|
||||||
|
teamId: string;
|
||||||
|
channelIds: string[];
|
||||||
|
};
|
||||||
|
notification: TNotification;
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TProjectMicrosoftTeamsConfigDALFactory = ReturnType<typeof projectMicrosoftTeamsConfigDALFactory>;
|
||||||
|
|
||||||
|
export const projectMicrosoftTeamsConfigDALFactory = (db: TDbClient) => {
|
||||||
|
const projectMicrosoftTeamsConfigOrm = ormify(db, TableName.ProjectMicrosoftTeamsConfigs);
|
||||||
|
|
||||||
|
const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => {
|
||||||
|
return (tx || db.replicaNode())(TableName.ProjectMicrosoftTeamsConfigs)
|
||||||
|
.join(
|
||||||
|
TableName.MicrosoftTeamsIntegrations,
|
||||||
|
`${TableName.ProjectMicrosoftTeamsConfigs}.microsoftTeamsIntegrationId`,
|
||||||
|
`${TableName.MicrosoftTeamsIntegrations}.id`
|
||||||
|
)
|
||||||
|
.where("projectId", "=", projectId)
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.ProjectMicrosoftTeamsConfigs),
|
||||||
|
selectAllTableCols(TableName.MicrosoftTeamsIntegrations)
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...projectMicrosoftTeamsConfigOrm, getIntegrationDetailsByProject };
|
||||||
|
};
|
||||||
@@ -698,6 +698,8 @@ export const orgServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||||
|
|
||||||
|
const invitingUser = await userDAL.findOne({ id: actorId });
|
||||||
|
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
|
|
||||||
const [inviteeOrgMembership] = await orgDAL.findMembership({
|
const [inviteeOrgMembership] = await orgDAL.findMembership({
|
||||||
@@ -731,8 +733,8 @@ export const orgServiceFactory = ({
|
|||||||
subjectLine: "Infisical organization invitation",
|
subjectLine: "Infisical organization invitation",
|
||||||
recipients: [inviteeOrgMembership.email as string],
|
recipients: [inviteeOrgMembership.email as string],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
inviterFirstName: inviteeOrgMembership.firstName,
|
inviterFirstName: invitingUser.firstName,
|
||||||
inviterUsername: inviteeOrgMembership.email,
|
inviterUsername: invitingUser.email,
|
||||||
organizationName: org?.name,
|
organizationName: org?.name,
|
||||||
email: inviteeOrgMembership.email,
|
email: inviteeOrgMembership.email,
|
||||||
organizationId: org?.id.toString(),
|
organizationId: org?.id.toString(),
|
||||||
@@ -761,6 +763,8 @@ export const orgServiceFactory = ({
|
|||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
|
||||||
|
const invitingUser = await userDAL.findOne({ id: actorId });
|
||||||
|
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
|
|
||||||
const isEmailInvalid = await isDisposableEmail(inviteeEmails);
|
const isEmailInvalid = await isDisposableEmail(inviteeEmails);
|
||||||
@@ -1179,8 +1183,8 @@ export const orgServiceFactory = ({
|
|||||||
subjectLine: "Infisical organization invitation",
|
subjectLine: "Infisical organization invitation",
|
||||||
recipients: [el.email],
|
recipients: [el.email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
inviterFirstName: el.firstName,
|
inviterFirstName: invitingUser.firstName,
|
||||||
inviterUsername: el.email,
|
inviterUsername: invitingUser.email,
|
||||||
organizationName: org?.name,
|
organizationName: org?.name,
|
||||||
email: el.email,
|
email: el.email,
|
||||||
organizationId: org?.id.toString(),
|
organizationId: org?.id.toString(),
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/s
|
|||||||
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
||||||
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||||
import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
||||||
|
import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
@@ -43,6 +44,9 @@ import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
|||||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns";
|
||||||
|
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
|
||||||
|
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
||||||
@@ -60,9 +64,11 @@ import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
|
|||||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
||||||
|
import { validateSlackChannelsField } from "../slack/slack-auth-validators";
|
||||||
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||||
import { TProjectDALFactory } from "./project-dal";
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns";
|
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns";
|
||||||
import { TProjectQueueFactory } from "./project-queue";
|
import { TProjectQueueFactory } from "./project-queue";
|
||||||
@@ -70,10 +76,11 @@ import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
|
|||||||
import {
|
import {
|
||||||
TCreateProjectDTO,
|
TCreateProjectDTO,
|
||||||
TDeleteProjectDTO,
|
TDeleteProjectDTO,
|
||||||
|
TDeleteProjectWorkflowIntegration,
|
||||||
TGetProjectDTO,
|
TGetProjectDTO,
|
||||||
TGetProjectKmsKey,
|
TGetProjectKmsKey,
|
||||||
TGetProjectSlackConfig,
|
|
||||||
TGetProjectSshConfig,
|
TGetProjectSshConfig,
|
||||||
|
TGetProjectWorkflowIntegrationConfig,
|
||||||
TListProjectAlertsDTO,
|
TListProjectAlertsDTO,
|
||||||
TListProjectCasDTO,
|
TListProjectCasDTO,
|
||||||
TListProjectCertificateTemplatesDTO,
|
TListProjectCertificateTemplatesDTO,
|
||||||
@@ -92,9 +99,9 @@ import {
|
|||||||
TUpdateProjectDTO,
|
TUpdateProjectDTO,
|
||||||
TUpdateProjectKmsDTO,
|
TUpdateProjectKmsDTO,
|
||||||
TUpdateProjectNameDTO,
|
TUpdateProjectNameDTO,
|
||||||
TUpdateProjectSlackConfig,
|
|
||||||
TUpdateProjectSshConfig,
|
TUpdateProjectSshConfig,
|
||||||
TUpdateProjectVersionLimitDTO,
|
TUpdateProjectVersionLimitDTO,
|
||||||
|
TUpdateProjectWorkflowIntegration,
|
||||||
TUpgradeProjectDTO
|
TUpgradeProjectDTO
|
||||||
} from "./project-types";
|
} from "./project-types";
|
||||||
|
|
||||||
@@ -123,8 +130,19 @@ type TProjectServiceFactoryDep = {
|
|||||||
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
|
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
|
||||||
>;
|
>;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "delete">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "delete">;
|
||||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
|
projectSlackConfigDAL: Pick<
|
||||||
|
TProjectSlackConfigDALFactory,
|
||||||
|
"findOne" | "transaction" | "updateById" | "create" | "delete"
|
||||||
|
>;
|
||||||
|
projectMicrosoftTeamsConfigDAL: Pick<
|
||||||
|
TProjectMicrosoftTeamsConfigDALFactory,
|
||||||
|
"findOne" | "transaction" | "updateById" | "create" | "delete"
|
||||||
|
>;
|
||||||
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
|
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
|
||||||
|
microsoftTeamsIntegrationDAL: Pick<
|
||||||
|
TMicrosoftTeamsIntegrationDALFactory,
|
||||||
|
"findById" | "findByIdWithWorkflowIntegrationDetails"
|
||||||
|
>;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||||
@@ -136,12 +154,12 @@ type TProjectServiceFactoryDep = {
|
|||||||
sshCertificateDAL: Pick<TSshCertificateDALFactory, "find" | "countSshCertificatesInProject">;
|
sshCertificateDAL: Pick<TSshCertificateDALFactory, "find" | "countSshCertificatesInProject">;
|
||||||
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find">;
|
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find">;
|
||||||
sshHostDAL: Pick<TSshHostDALFactory, "find" | "findSshHostsWithLoginMappings">;
|
sshHostDAL: Pick<TSshHostDALFactory, "find" | "findSshHostsWithLoginMappings">;
|
||||||
|
sshHostGroupDAL: Pick<TSshHostGroupDALFactory, "find" | "findSshHostGroupsWithLoginMappings">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
|
|
||||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
||||||
@@ -193,11 +211,14 @@ export const projectServiceFactory = ({
|
|||||||
sshCertificateDAL,
|
sshCertificateDAL,
|
||||||
sshCertificateTemplateDAL,
|
sshCertificateTemplateDAL,
|
||||||
sshHostDAL,
|
sshHostDAL,
|
||||||
|
sshHostGroupDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
slackIntegrationDAL,
|
slackIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
projectTemplateService,
|
projectTemplateService,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
smtpService
|
smtpService
|
||||||
@@ -1143,6 +1164,32 @@ export const projectServiceFactory = ({
|
|||||||
return allowedHosts;
|
return allowedHosts;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of SSH host groups for project
|
||||||
|
*/
|
||||||
|
const listProjectSshHostGroups = async ({
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
projectId
|
||||||
|
}: TListProjectSshHostsDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHostGroups);
|
||||||
|
|
||||||
|
const sshHostGroups = await sshHostGroupDAL.findSshHostGroupsWithLoginMappings(projectId);
|
||||||
|
|
||||||
|
return sshHostGroups;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of SSH certificates for project
|
* Return list of SSH certificates for project
|
||||||
*/
|
*/
|
||||||
@@ -1452,13 +1499,14 @@ export const projectServiceFactory = ({
|
|||||||
return projectSshConfig;
|
return projectSshConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjectSlackConfig = async ({
|
const getProjectWorkflowIntegrationConfig = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
projectId
|
projectId,
|
||||||
}: TGetProjectSlackConfig) => {
|
integration
|
||||||
|
}: TGetProjectWorkflowIntegrationConfig) => {
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@@ -1477,23 +1525,60 @@ export const projectServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
return projectSlackConfigDAL.findOne({
|
if (integration === WorkflowIntegration.SLACK) {
|
||||||
projectId: project.id
|
const config = await projectSlackConfigDAL.findOne({
|
||||||
|
projectId: project.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Workflow integration config for project '${projectId}' and integration '${integration}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
integration,
|
||||||
|
integrationId: config.slackIntegrationId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
|
||||||
|
const config = await projectMicrosoftTeamsConfigDAL.findOne({
|
||||||
|
projectId: project.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Workflow integration config for project '${projectId}' and integration '${integration}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
integration,
|
||||||
|
integrationId: config.microsoftTeamsIntegrationId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Integration type '${integration as string}' not supported`
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateProjectSlackConfig = async ({
|
const updateProjectWorkflowIntegration = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
projectId,
|
projectId,
|
||||||
slackIntegrationId,
|
integration,
|
||||||
|
integrationId,
|
||||||
isAccessRequestNotificationEnabled,
|
isAccessRequestNotificationEnabled,
|
||||||
accessRequestChannels,
|
accessRequestChannels,
|
||||||
isSecretRequestNotificationEnabled,
|
isSecretRequestNotificationEnabled,
|
||||||
secretRequestChannels
|
secretRequestChannels
|
||||||
}: TUpdateProjectSlackConfig) => {
|
}: TUpdateProjectWorkflowIntegration) => {
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@@ -1501,17 +1586,206 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(slackIntegrationId);
|
if (integration === WorkflowIntegration.SLACK) {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
if (!slackIntegration) {
|
actor,
|
||||||
throw new NotFoundError({
|
actorId,
|
||||||
message: `Slack integration with ID '${slackIntegrationId}' not found`
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
const sanitizedAccessRequestChannels = validateSlackChannelsField.parse(accessRequestChannels);
|
||||||
|
const sanitizedSecretRequestChannels = validateSlackChannelsField.parse(secretRequestChannels);
|
||||||
|
|
||||||
|
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
|
||||||
|
|
||||||
|
if (!slackIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Slack integration with ID '${integrationId}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slackIntegration.orgId !== actorOrgId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Selected slack integration is not in the same organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slackIntegration.orgId !== project.orgId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Selected slack integration is not in the same organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedWorkflowIntegration = await projectSlackConfigDAL.transaction(async (tx) => {
|
||||||
|
const slackConfig = await projectSlackConfigDAL.findOne(
|
||||||
|
{
|
||||||
|
projectId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (slackConfig) {
|
||||||
|
return projectSlackConfigDAL.updateById(
|
||||||
|
slackConfig.id,
|
||||||
|
{
|
||||||
|
slackIntegrationId: integrationId,
|
||||||
|
isAccessRequestNotificationEnabled,
|
||||||
|
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||||
|
isSecretRequestNotificationEnabled,
|
||||||
|
secretRequestChannels: sanitizedSecretRequestChannels
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectSlackConfigDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
slackIntegrationId: integrationId,
|
||||||
|
isAccessRequestNotificationEnabled,
|
||||||
|
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||||
|
isSecretRequestNotificationEnabled,
|
||||||
|
secretRequestChannels: sanitizedSecretRequestChannels
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updatedWorkflowIntegration,
|
||||||
|
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||||
|
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||||
|
integrationId: slackIntegration.id,
|
||||||
|
integration: WorkflowIntegration.SLACK
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.Any
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
if (isAccessRequestNotificationEnabled && !accessRequestChannels) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Access request channels are required when access request notifications are enabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSecretRequestNotificationEnabled && !secretRequestChannels) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Secret request channels are required when secret request notifications are enabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secretRequestChannels && !accessRequestChannels) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "At least one of access request channels or secret request channels is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const microsoftTeamsIntegration =
|
||||||
|
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
|
||||||
|
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID '${integrationId}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (microsoftTeamsIntegration.status !== WorkflowIntegrationStatus.INSTALLED) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams integration is not properly installed in your tenant."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (microsoftTeamsIntegration.orgId !== actorOrgId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Selected Microsoft Teams integration is not in the same organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (microsoftTeamsIntegration.orgId !== project.orgId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Selected Microsoft Teams integration is not in the same organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedAccessRequestChannels = validateMicrosoftTeamsChannelsSchema.parse(accessRequestChannels);
|
||||||
|
const sanitizedSecretRequestChannels = validateMicrosoftTeamsChannelsSchema.parse(secretRequestChannels);
|
||||||
|
|
||||||
|
const updatedWorkflowIntegration = await projectMicrosoftTeamsConfigDAL.transaction(async (tx) => {
|
||||||
|
const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.findOne(
|
||||||
|
{
|
||||||
|
projectId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (microsoftTeamsConfig) {
|
||||||
|
return projectMicrosoftTeamsConfigDAL.updateById(
|
||||||
|
microsoftTeamsConfig.id,
|
||||||
|
{
|
||||||
|
microsoftTeamsIntegrationId: integrationId,
|
||||||
|
isAccessRequestNotificationEnabled,
|
||||||
|
accessRequestChannels: sanitizedAccessRequestChannels || {},
|
||||||
|
isSecretRequestNotificationEnabled,
|
||||||
|
secretRequestChannels: sanitizedSecretRequestChannels || {}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectMicrosoftTeamsConfigDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
microsoftTeamsIntegrationId: integrationId,
|
||||||
|
isAccessRequestNotificationEnabled,
|
||||||
|
accessRequestChannels: sanitizedAccessRequestChannels || {},
|
||||||
|
isSecretRequestNotificationEnabled,
|
||||||
|
secretRequestChannels: sanitizedSecretRequestChannels || {}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updatedWorkflowIntegration,
|
||||||
|
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||||
|
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||||
|
integrationId: microsoftTeamsIntegration.id,
|
||||||
|
integration: WorkflowIntegration.MICROSOFT_TEAMS
|
||||||
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slackIntegration.orgId !== actorOrgId) {
|
throw new BadRequestError({
|
||||||
throw new ForbiddenRequestError({
|
message: `Integration type '${integration as string}' not supported`
|
||||||
message: "Selected slack integration is not in the same organization"
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProjectWorkflowIntegration = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
integrationId,
|
||||||
|
integration
|
||||||
|
}: TDeleteProjectWorkflowIntegration) => {
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Project with ID '${projectId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1524,47 +1798,28 @@ export const projectServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
if (slackIntegration.orgId !== project.orgId) {
|
if (integration === WorkflowIntegration.SLACK) {
|
||||||
throw new ForbiddenRequestError({
|
const [deletedIntegration] = await projectSlackConfigDAL.delete({
|
||||||
message: "Selected slack integration is not in the same organization"
|
projectId,
|
||||||
|
slackIntegrationId: integrationId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return deletedIntegration;
|
||||||
}
|
}
|
||||||
|
|
||||||
return projectSlackConfigDAL.transaction(async (tx) => {
|
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
|
||||||
const slackConfig = await projectSlackConfigDAL.findOne(
|
const [deletedIntegration] = await projectMicrosoftTeamsConfigDAL.delete({
|
||||||
{
|
projectId,
|
||||||
projectId
|
microsoftTeamsIntegrationId: integrationId
|
||||||
},
|
});
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
if (slackConfig) {
|
return deletedIntegration;
|
||||||
return projectSlackConfigDAL.updateById(
|
}
|
||||||
slackConfig.id,
|
|
||||||
{
|
|
||||||
slackIntegrationId,
|
|
||||||
isAccessRequestNotificationEnabled,
|
|
||||||
accessRequestChannels,
|
|
||||||
isSecretRequestNotificationEnabled,
|
|
||||||
secretRequestChannels
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return projectSlackConfigDAL.create(
|
throw new BadRequestError({
|
||||||
{
|
message: `Integration with ID '${integrationId}' not found`
|
||||||
projectId,
|
|
||||||
slackIntegrationId,
|
|
||||||
isAccessRequestNotificationEnabled,
|
|
||||||
accessRequestChannels,
|
|
||||||
isSecretRequestNotificationEnabled,
|
|
||||||
secretRequestChannels
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1665,6 +1920,7 @@ export const projectServiceFactory = ({
|
|||||||
listProjectCertificateTemplates,
|
listProjectCertificateTemplates,
|
||||||
listProjectSshCas,
|
listProjectSshCas,
|
||||||
listProjectSshHosts,
|
listProjectSshHosts,
|
||||||
|
listProjectSshHostGroups,
|
||||||
listProjectSshCertificates,
|
listProjectSshCertificates,
|
||||||
listProjectSshCertificateTemplates,
|
listProjectSshCertificateTemplates,
|
||||||
updateVersionLimit,
|
updateVersionLimit,
|
||||||
@@ -1673,10 +1929,11 @@ export const projectServiceFactory = ({
|
|||||||
getProjectKmsBackup,
|
getProjectKmsBackup,
|
||||||
loadProjectKmsBackup,
|
loadProjectKmsBackup,
|
||||||
getProjectKmsKeys,
|
getProjectKmsKeys,
|
||||||
|
getProjectWorkflowIntegrationConfig,
|
||||||
|
updateProjectWorkflowIntegration,
|
||||||
|
deleteProjectWorkflowIntegration,
|
||||||
getProjectSshConfig,
|
getProjectSshConfig,
|
||||||
updateProjectSshConfig,
|
updateProjectSshConfig,
|
||||||
getProjectSlackConfig,
|
|
||||||
updateProjectSlackConfig,
|
|
||||||
requestProjectAccess,
|
requestProjectAccess,
|
||||||
searchProjects
|
searchProjects
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
|||||||
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types";
|
||||||
|
|
||||||
enum KmsType {
|
enum KmsType {
|
||||||
External = "external",
|
External = "external",
|
||||||
@@ -166,14 +167,33 @@ export type TUpdateProjectSshConfig = {
|
|||||||
|
|
||||||
export type TGetProjectSshConfig = TProjectPermission;
|
export type TGetProjectSshConfig = TProjectPermission;
|
||||||
|
|
||||||
export type TGetProjectSlackConfig = TProjectPermission;
|
export type TGetProjectWorkflowIntegrationConfig = TProjectPermission & {
|
||||||
|
integration: WorkflowIntegration;
|
||||||
|
};
|
||||||
|
|
||||||
export type TUpdateProjectSlackConfig = {
|
export type TUpdateProjectWorkflowIntegration = (
|
||||||
slackIntegrationId: string;
|
| {
|
||||||
isAccessRequestNotificationEnabled: boolean;
|
integrationId: string;
|
||||||
accessRequestChannels: string;
|
integration: WorkflowIntegration.SLACK;
|
||||||
isSecretRequestNotificationEnabled: boolean;
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
secretRequestChannels: string;
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
|
accessRequestChannels?: string;
|
||||||
|
secretRequestChannels?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
integrationId: string;
|
||||||
|
integration: WorkflowIntegration.MICROSOFT_TEAMS;
|
||||||
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
|
accessRequestChannels?: { teamId: string; channelIds: string[] };
|
||||||
|
secretRequestChannels?: { teamId: string; channelIds: string[] };
|
||||||
|
}
|
||||||
|
) &
|
||||||
|
TProjectPermission;
|
||||||
|
|
||||||
|
export type TDeleteProjectWorkflowIntegration = {
|
||||||
|
integrationId: string;
|
||||||
|
integration: WorkflowIntegration;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TBootstrapSshProjectDTO = {
|
export type TBootstrapSshProjectDTO = {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
export const HC_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||||
|
name: "Hashicorp Vault",
|
||||||
|
destination: SecretSync.HCVault,
|
||||||
|
connection: AppConnection.HCVault,
|
||||||
|
canImportSecrets: true
|
||||||
|
};
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { isAxiosError } from "axios";
|
||||||
|
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
|
import { getHCVaultAccessToken, getHCVaultInstanceUrl } from "@app/services/app-connection/hc-vault";
|
||||||
|
import {
|
||||||
|
THCVaultListVariables,
|
||||||
|
THCVaultListVariablesResponse,
|
||||||
|
THCVaultSyncWithCredentials,
|
||||||
|
TPostHCVaultVariable
|
||||||
|
} from "@app/services/secret-sync/hc-vault/hc-vault-sync-types";
|
||||||
|
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||||
|
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
const listHCVaultVariables = async ({ instanceUrl, namespace, mount, accessToken, path }: THCVaultListVariables) => {
|
||||||
|
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await request.get<THCVaultListVariablesResponse>(
|
||||||
|
`${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
...(namespace ? { "X-Vault-Namespace": namespace } : {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.data.data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// Returning an empty set when a path isn't found allows that path to be created by a later POST request
|
||||||
|
if (isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hashicorp Vault updates all variables in one batch. This is to respect their versioning
|
||||||
|
const updateHCVaultVariables = async ({
|
||||||
|
path,
|
||||||
|
instanceUrl,
|
||||||
|
namespace,
|
||||||
|
accessToken,
|
||||||
|
mount,
|
||||||
|
data
|
||||||
|
}: TPostHCVaultVariable) => {
|
||||||
|
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||||
|
|
||||||
|
return request.post(
|
||||||
|
`${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`,
|
||||||
|
{
|
||||||
|
data
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
...(namespace ? { "X-Vault-Namespace": namespace } : {}),
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HCVaultSyncFns = {
|
||||||
|
syncSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
|
const {
|
||||||
|
connection,
|
||||||
|
destinationConfig: { mount, path },
|
||||||
|
syncOptions: { disableSecretDeletion }
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
const { namespace } = connection.credentials;
|
||||||
|
const accessToken = await getHCVaultAccessToken(connection);
|
||||||
|
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||||
|
|
||||||
|
const variables = await listHCVaultVariables({
|
||||||
|
instanceUrl,
|
||||||
|
accessToken,
|
||||||
|
namespace,
|
||||||
|
mount,
|
||||||
|
path
|
||||||
|
});
|
||||||
|
let tainted = false;
|
||||||
|
|
||||||
|
for (const entry of Object.entries(secretMap)) {
|
||||||
|
const [key, { value }] = entry;
|
||||||
|
if (value !== variables[key]) {
|
||||||
|
variables[key] = value;
|
||||||
|
tainted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disableSecretDeletion) return;
|
||||||
|
|
||||||
|
for await (const [key] of Object.entries(variables)) {
|
||||||
|
if (!(key in secretMap)) {
|
||||||
|
delete variables[key];
|
||||||
|
tainted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update variables if there was a change detected
|
||||||
|
if (!tainted) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateHCVaultVariables({ accessToken, instanceUrl, namespace, mount, path, data: variables });
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
|
const {
|
||||||
|
connection,
|
||||||
|
destinationConfig: { mount, path }
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
const { namespace } = connection.credentials;
|
||||||
|
const accessToken = await getHCVaultAccessToken(connection);
|
||||||
|
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||||
|
|
||||||
|
const variables = await listHCVaultVariables({ instanceUrl, namespace, accessToken, mount, path });
|
||||||
|
|
||||||
|
for await (const [key] of Object.entries(variables)) {
|
||||||
|
if (key in secretMap) {
|
||||||
|
delete variables[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateHCVaultVariables({ accessToken, instanceUrl, namespace, mount, path, data: variables });
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getSecrets: async (secretSync: THCVaultSyncWithCredentials) => {
|
||||||
|
const {
|
||||||
|
connection,
|
||||||
|
destinationConfig: { mount, path }
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
const { namespace } = connection.credentials;
|
||||||
|
const accessToken = await getHCVaultAccessToken(connection);
|
||||||
|
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||||
|
|
||||||
|
const variables = await listHCVaultVariables({
|
||||||
|
instanceUrl,
|
||||||
|
namespace,
|
||||||
|
accessToken,
|
||||||
|
mount,
|
||||||
|
path
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.fromEntries(Object.entries(variables).map(([key, value]) => [key, { value }]));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretSyncs } from "@app/lib/api-docs";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import {
|
||||||
|
BaseSecretSyncSchema,
|
||||||
|
GenericCreateSecretSyncFieldsSchema,
|
||||||
|
GenericUpdateSecretSyncFieldsSchema
|
||||||
|
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||||
|
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
const HCVaultSyncDestinationConfigSchema = z.object({
|
||||||
|
mount: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Secrets Engine Mount required")
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.HC_VAULT.mount),
|
||||||
|
path: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Path required")
|
||||||
|
.transform((val) => val.replace(/^\/+|\/+$/g, "")) // removes leading/trailing slashes
|
||||||
|
.refine((val) => new RE2("^([a-zA-Z0-9._-]+/)*[a-zA-Z0-9._-]+$").test(val), {
|
||||||
|
message:
|
||||||
|
"Invalid Vault path format. Use alphanumerics, dots, dashes, underscores, and single slashes between segments."
|
||||||
|
})
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.HC_VAULT.path)
|
||||||
|
});
|
||||||
|
|
||||||
|
const HCVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||||
|
|
||||||
|
export const HCVaultSyncSchema = BaseSecretSyncSchema(SecretSync.HCVault, HCVaultSyncOptionsConfig).extend({
|
||||||
|
destination: z.literal(SecretSync.HCVault),
|
||||||
|
destinationConfig: HCVaultSyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateHCVaultSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.HCVault,
|
||||||
|
HCVaultSyncOptionsConfig
|
||||||
|
).extend({
|
||||||
|
destinationConfig: HCVaultSyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateHCVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.HCVault,
|
||||||
|
HCVaultSyncOptionsConfig
|
||||||
|
).extend({
|
||||||
|
destinationConfig: HCVaultSyncDestinationConfigSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const HCVaultSyncListItemSchema = z.object({
|
||||||
|
name: z.literal("Hashicorp Vault"),
|
||||||
|
connection: z.literal(AppConnection.HCVault),
|
||||||
|
destination: z.literal(SecretSync.HCVault),
|
||||||
|
canImportSecrets: z.literal(true)
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { THCVaultConnection } from "@app/services/app-connection/hc-vault";
|
||||||
|
|
||||||
|
import { CreateHCVaultSyncSchema, HCVaultSyncListItemSchema, HCVaultSyncSchema } from "./hc-vault-sync-schemas";
|
||||||
|
|
||||||
|
export type THCVaultSync = z.infer<typeof HCVaultSyncSchema>;
|
||||||
|
|
||||||
|
export type THCVaultSyncInput = z.infer<typeof CreateHCVaultSyncSchema>;
|
||||||
|
|
||||||
|
export type THCVaultSyncListItem = z.infer<typeof HCVaultSyncListItemSchema>;
|
||||||
|
|
||||||
|
export type THCVaultSyncWithCredentials = THCVaultSync & {
|
||||||
|
connection: THCVaultConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type THCVaultListVariablesResponse = {
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type THCVaultListVariables = {
|
||||||
|
accessToken: string;
|
||||||
|
instanceUrl: string;
|
||||||
|
namespace?: string;
|
||||||
|
mount: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPostHCVaultVariable = THCVaultListVariables & {
|
||||||
|
data: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteHCVaultVariable = THCVaultListVariables;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./hc-vault-sync-constants";
|
||||||
|
export * from "./hc-vault-sync-fns";
|
||||||
|
export * from "./hc-vault-sync-schemas";
|
||||||
|
export * from "./hc-vault-sync-types";
|
||||||
@@ -11,6 +11,7 @@ export enum SecretSync {
|
|||||||
Camunda = "camunda",
|
Camunda = "camunda",
|
||||||
Vercel = "vercel",
|
Vercel = "vercel",
|
||||||
Windmill = "windmill",
|
Windmill = "windmill",
|
||||||
|
HCVault = "hashicorp-vault",
|
||||||
TeamCity = "teamcity"
|
TeamCity = "teamcity"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./az
|
|||||||
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
|
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
|
||||||
import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
||||||
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||||
|
import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
|
||||||
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||||
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||||
import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity";
|
import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity";
|
||||||
@@ -45,6 +46,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
|||||||
[SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION,
|
[SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION,
|
||||||
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION,
|
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION,
|
||||||
[SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION,
|
[SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION,
|
||||||
|
[SecretSync.HCVault]: HC_VAULT_SYNC_LIST_OPTION,
|
||||||
[SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION
|
[SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,6 +144,8 @@ export const SecretSyncFns = {
|
|||||||
return VercelSyncFns.syncSecrets(secretSync, secretMap);
|
return VercelSyncFns.syncSecrets(secretSync, secretMap);
|
||||||
case SecretSync.Windmill:
|
case SecretSync.Windmill:
|
||||||
return WindmillSyncFns.syncSecrets(secretSync, secretMap);
|
return WindmillSyncFns.syncSecrets(secretSync, secretMap);
|
||||||
|
case SecretSync.HCVault:
|
||||||
|
return HCVaultSyncFns.syncSecrets(secretSync, secretMap);
|
||||||
case SecretSync.TeamCity:
|
case SecretSync.TeamCity:
|
||||||
return TeamCitySyncFns.syncSecrets(secretSync, secretMap);
|
return TeamCitySyncFns.syncSecrets(secretSync, secretMap);
|
||||||
default:
|
default:
|
||||||
@@ -203,6 +207,9 @@ export const SecretSyncFns = {
|
|||||||
case SecretSync.Windmill:
|
case SecretSync.Windmill:
|
||||||
secretMap = await WindmillSyncFns.getSecrets(secretSync);
|
secretMap = await WindmillSyncFns.getSecrets(secretSync);
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.HCVault:
|
||||||
|
secretMap = await HCVaultSyncFns.getSecrets(secretSync);
|
||||||
|
break;
|
||||||
case SecretSync.TeamCity:
|
case SecretSync.TeamCity:
|
||||||
secretMap = await TeamCitySyncFns.getSecrets(secretSync);
|
secretMap = await TeamCitySyncFns.getSecrets(secretSync);
|
||||||
break;
|
break;
|
||||||
@@ -259,6 +266,8 @@ export const SecretSyncFns = {
|
|||||||
return VercelSyncFns.removeSecrets(secretSync, secretMap);
|
return VercelSyncFns.removeSecrets(secretSync, secretMap);
|
||||||
case SecretSync.Windmill:
|
case SecretSync.Windmill:
|
||||||
return WindmillSyncFns.removeSecrets(secretSync, secretMap);
|
return WindmillSyncFns.removeSecrets(secretSync, secretMap);
|
||||||
|
case SecretSync.HCVault:
|
||||||
|
return HCVaultSyncFns.removeSecrets(secretSync, secretMap);
|
||||||
case SecretSync.TeamCity:
|
case SecretSync.TeamCity:
|
||||||
return TeamCitySyncFns.removeSecrets(secretSync, secretMap);
|
return TeamCitySyncFns.removeSecrets(secretSync, secretMap);
|
||||||
default:
|
default:
|
||||||
@@ -282,7 +291,7 @@ export const parseSyncErrorMessage = (err: unknown): string => {
|
|||||||
} else if (err instanceof AxiosError) {
|
} else if (err instanceof AxiosError) {
|
||||||
errorMessage = err?.response?.data
|
errorMessage = err?.response?.data
|
||||||
? JSON.stringify(err?.response?.data)
|
? JSON.stringify(err?.response?.data)
|
||||||
: err?.message ?? "An unknown error occurred.";
|
: (err?.message ?? "An unknown error occurred.");
|
||||||
} else {
|
} else {
|
||||||
errorMessage = (err as Error)?.message || "An unknown error occurred.";
|
errorMessage = (err as Error)?.message || "An unknown error occurred.";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
|||||||
[SecretSync.Camunda]: "Camunda",
|
[SecretSync.Camunda]: "Camunda",
|
||||||
[SecretSync.Vercel]: "Vercel",
|
[SecretSync.Vercel]: "Vercel",
|
||||||
[SecretSync.Windmill]: "Windmill",
|
[SecretSync.Windmill]: "Windmill",
|
||||||
|
[SecretSync.HCVault]: "Hashicorp Vault",
|
||||||
[SecretSync.TeamCity]: "TeamCity"
|
[SecretSync.TeamCity]: "TeamCity"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,5 +31,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
|||||||
[SecretSync.Camunda]: AppConnection.Camunda,
|
[SecretSync.Camunda]: AppConnection.Camunda,
|
||||||
[SecretSync.Vercel]: AppConnection.Vercel,
|
[SecretSync.Vercel]: AppConnection.Vercel,
|
||||||
[SecretSync.Windmill]: AppConnection.Windmill,
|
[SecretSync.Windmill]: AppConnection.Windmill,
|
||||||
|
[SecretSync.HCVault]: AppConnection.HCVault,
|
||||||
[SecretSync.TeamCity]: AppConnection.TeamCity
|
[SecretSync.TeamCity]: AppConnection.TeamCity
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -834,7 +834,7 @@ export const secretSyncQueueFactory = ({
|
|||||||
secretPath: folder?.path,
|
secretPath: folder?.path,
|
||||||
environment: environment?.name,
|
environment: environment?.name,
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
syncUrl: `${appCfg.SITE_URL}/integrations/secret-syncs/${destination}/${secretSync.id}`
|
syncUrl: `${appCfg.SITE_URL}/secret-manager/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ import {
|
|||||||
TAzureKeyVaultSyncWithCredentials
|
TAzureKeyVaultSyncWithCredentials
|
||||||
} from "./azure-key-vault";
|
} from "./azure-key-vault";
|
||||||
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
|
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
|
||||||
|
import {
|
||||||
|
THCVaultSync,
|
||||||
|
THCVaultSyncInput,
|
||||||
|
THCVaultSyncListItem,
|
||||||
|
THCVaultSyncWithCredentials
|
||||||
|
} from "./hc-vault/hc-vault-sync-types";
|
||||||
import {
|
import {
|
||||||
THumanitecSync,
|
THumanitecSync,
|
||||||
THumanitecSyncInput,
|
THumanitecSyncInput,
|
||||||
@@ -88,6 +94,7 @@ export type TSecretSync =
|
|||||||
| TCamundaSync
|
| TCamundaSync
|
||||||
| TVercelSync
|
| TVercelSync
|
||||||
| TWindmillSync
|
| TWindmillSync
|
||||||
|
| THCVaultSync
|
||||||
| TTeamCitySync;
|
| TTeamCitySync;
|
||||||
|
|
||||||
export type TSecretSyncWithCredentials =
|
export type TSecretSyncWithCredentials =
|
||||||
@@ -103,6 +110,7 @@ export type TSecretSyncWithCredentials =
|
|||||||
| TCamundaSyncWithCredentials
|
| TCamundaSyncWithCredentials
|
||||||
| TVercelSyncWithCredentials
|
| TVercelSyncWithCredentials
|
||||||
| TWindmillSyncWithCredentials
|
| TWindmillSyncWithCredentials
|
||||||
|
| THCVaultSyncWithCredentials
|
||||||
| TTeamCitySyncWithCredentials;
|
| TTeamCitySyncWithCredentials;
|
||||||
|
|
||||||
export type TSecretSyncInput =
|
export type TSecretSyncInput =
|
||||||
@@ -118,6 +126,7 @@ export type TSecretSyncInput =
|
|||||||
| TCamundaSyncInput
|
| TCamundaSyncInput
|
||||||
| TVercelSyncInput
|
| TVercelSyncInput
|
||||||
| TWindmillSyncInput
|
| TWindmillSyncInput
|
||||||
|
| THCVaultSyncInput
|
||||||
| TTeamCitySyncInput;
|
| TTeamCitySyncInput;
|
||||||
|
|
||||||
export type TSecretSyncListItem =
|
export type TSecretSyncListItem =
|
||||||
@@ -133,6 +142,7 @@ export type TSecretSyncListItem =
|
|||||||
| TCamundaSyncListItem
|
| TCamundaSyncListItem
|
||||||
| TVercelSyncListItem
|
| TVercelSyncListItem
|
||||||
| TWindmillSyncListItem
|
| TWindmillSyncListItem
|
||||||
|
| THCVaultSyncListItem
|
||||||
| TTeamCitySyncListItem;
|
| TTeamCitySyncListItem;
|
||||||
|
|
||||||
export type TSyncOptionsConfig = {
|
export type TSyncOptionsConfig = {
|
||||||
|
|||||||
@@ -740,7 +740,7 @@ export const secretQueueFactory = ({
|
|||||||
environment: jobPayload.environmentName,
|
environment: jobPayload.environmentName,
|
||||||
count: jobPayload.count,
|
count: jobPayload.count,
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
integrationUrl: `${appCfg.SITE_URL}/integrations/${project.id}`
|
integrationUrl: `${appCfg.SITE_URL}/secret-manager/${project.id}/integrations?selectedTab=native-integrations`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ import { WebClient } from "@slack/web-api";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||||
|
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TSendSlackNotificationDTO } from "./slack-types";
|
||||||
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
|
|
||||||
import { SlackTriggerFeature, TSlackNotification } from "./slack-types";
|
|
||||||
|
|
||||||
export const fetchSlackChannels = async (botKey: string) => {
|
export const fetchSlackChannels = async (botKey: string) => {
|
||||||
const slackChannels: {
|
const slackChannels: {
|
||||||
@@ -41,11 +39,11 @@ export const fetchSlackChannels = async (botKey: string) => {
|
|||||||
return slackChannels;
|
return slackChannels;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildSlackPayload = (notification: TSlackNotification) => {
|
const buildSlackPayload = (notification: TNotification) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
case SlackTriggerFeature.SECRET_APPROVAL: {
|
case TriggerFeature.SECRET_APPROVAL: {
|
||||||
const { payload } = notification;
|
const { payload } = notification;
|
||||||
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
|
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
|
||||||
*Environment*: ${payload.environment}
|
*Environment*: ${payload.environment}
|
||||||
@@ -79,7 +77,7 @@ View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId
|
|||||||
payloadBlocks
|
payloadBlocks
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case SlackTriggerFeature.ACCESS_REQUEST: {
|
case TriggerFeature.ACCESS_REQUEST: {
|
||||||
const { payload } = notification;
|
const { payload } = notification;
|
||||||
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
|
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
|
||||||
payload.isTemporary ? "temporary" : "permanent"
|
payload.isTemporary ? "temporary" : "permanent"
|
||||||
@@ -125,51 +123,24 @@ User Note: ${payload.note}`
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const triggerSlackNotification = async ({
|
export const sendSlackNotification = async ({
|
||||||
projectId,
|
orgId,
|
||||||
notification,
|
notification,
|
||||||
projectSlackConfigDAL,
|
kmsService,
|
||||||
projectDAL,
|
targetChannelIds,
|
||||||
kmsService
|
slackIntegration
|
||||||
}: {
|
}: TSendSlackNotificationDTO) => {
|
||||||
projectId: string;
|
|
||||||
notification: TSlackNotification;
|
|
||||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
|
||||||
}) => {
|
|
||||||
const { payloadMessage, payloadBlocks } = buildSlackPayload(notification);
|
|
||||||
const project = await projectDAL.findById(projectId);
|
|
||||||
const slackIntegration = await projectSlackConfigDAL.getIntegrationDetailsByProject(project.id);
|
|
||||||
|
|
||||||
if (!slackIntegration) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetChannelIds: string[] = [];
|
|
||||||
if (notification.type === SlackTriggerFeature.ACCESS_REQUEST) {
|
|
||||||
targetChannelIds = slackIntegration.accessRequestChannels?.split(", ") || [];
|
|
||||||
if (!targetChannelIds.length || !slackIntegration.isAccessRequestNotificationEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (notification.type === SlackTriggerFeature.SECRET_APPROVAL) {
|
|
||||||
targetChannelIds = slackIntegration.secretRequestChannels?.split(", ") || [];
|
|
||||||
if (!targetChannelIds.length || !slackIntegration.isSecretRequestNotificationEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.Organization,
|
type: KmsDataKey.Organization,
|
||||||
orgId: project.orgId
|
orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
const botKey = orgDataKeyDecryptor({
|
const botKey = orgDataKeyDecryptor({
|
||||||
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
||||||
}).toString("utf8");
|
}).toString("utf8");
|
||||||
|
|
||||||
const slackWebClient = new WebClient(botKey);
|
const slackWebClient = new WebClient(botKey);
|
||||||
|
|
||||||
|
const { payloadMessage, payloadBlocks } = buildSlackPayload(notification);
|
||||||
|
|
||||||
for await (const conversationId of targetChannelIds) {
|
for await (const conversationId of targetChannelIds) {
|
||||||
// we send both text and blocks for compatibility with barebone clients
|
// we send both text and blocks for compatibility with barebone clients
|
||||||
await slackWebClient.chat
|
await slackWebClient.chat
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import { TSlackIntegrations } from "@app/db/schemas";
|
||||||
import { TOrgPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
import { TNotification } from "@app/lib/workflow-integrations/types";
|
||||||
|
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
|
||||||
export type TGetSlackInstallUrlDTO = {
|
export type TGetSlackInstallUrlDTO = {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -48,34 +52,10 @@ export type TReinstallSlackIntegrationDTO = {
|
|||||||
slackBotUserId: string;
|
slackBotUserId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum SlackTriggerFeature {
|
export type TSendSlackNotificationDTO = {
|
||||||
SECRET_APPROVAL = "secret-approval",
|
orgId: string;
|
||||||
ACCESS_REQUEST = "access-request"
|
notification: TNotification;
|
||||||
}
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
targetChannelIds: string[];
|
||||||
export type TSlackNotification =
|
slackIntegration: TSlackIntegrations;
|
||||||
| {
|
};
|
||||||
type: SlackTriggerFeature.SECRET_APPROVAL;
|
|
||||||
payload: {
|
|
||||||
userEmail: string;
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
requestId: string;
|
|
||||||
projectId: string;
|
|
||||||
secretKeys: string[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: SlackTriggerFeature.ACCESS_REQUEST;
|
|
||||||
payload: {
|
|
||||||
requesterFullName: string;
|
|
||||||
requesterEmail: string;
|
|
||||||
isTemporary: boolean;
|
|
||||||
secretPath: string;
|
|
||||||
environment: string;
|
|
||||||
projectName: string;
|
|
||||||
permissions: string[];
|
|
||||||
approvalUrl: string;
|
|
||||||
note?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { Button, Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface AccessApprovalRequestTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
projectName: string;
|
||||||
|
requesterFullName: string;
|
||||||
|
requesterEmail: string;
|
||||||
|
isTemporary: boolean;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
expiresIn: string;
|
||||||
|
permissions: string[];
|
||||||
|
note?: string;
|
||||||
|
approvalUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AccessApprovalRequestTemplate = ({
|
||||||
|
projectName,
|
||||||
|
siteUrl,
|
||||||
|
requesterFullName,
|
||||||
|
requesterEmail,
|
||||||
|
isTemporary,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
expiresIn,
|
||||||
|
permissions,
|
||||||
|
note,
|
||||||
|
approvalUrl
|
||||||
|
}: AccessApprovalRequestTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Access Approval Request"
|
||||||
|
preview="A new access approval request is pending your review."
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
You have a new access approval request pending review for the project <strong>{projectName}</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
<strong>{requesterFullName}</strong> (
|
||||||
|
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline">
|
||||||
|
{requesterEmail}
|
||||||
|
</Link>
|
||||||
|
) has requested {isTemporary ? "temporary" : "permanent"} access to <strong>{secretPath}</strong> in the{" "}
|
||||||
|
<strong>{environment}</strong> environment.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{isTemporary && (
|
||||||
|
<Text className="text-[14px] text-red-500 leading-[24px]">
|
||||||
|
<strong>This access will expire {expiresIn} after approval.</strong>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text className="text-[14px] leading-[24px] mb-[4px]">
|
||||||
|
<strong>The following permissions are requested:</strong>
|
||||||
|
</Text>
|
||||||
|
{permissions.map((permission) => (
|
||||||
|
<Text key={permission} className="text-[14px] my-[2px] leading-[24px]">
|
||||||
|
- {permission}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
{note && (
|
||||||
|
<Text className="text-[14px] text-slate-700 leading-[24px]">
|
||||||
|
<strong className="text-black">User Note:</strong> "{note}"
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[28px]">
|
||||||
|
<Button
|
||||||
|
href={approvalUrl}
|
||||||
|
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
|
||||||
|
>
|
||||||
|
Review Request
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccessApprovalRequestTemplate;
|
||||||
|
|
||||||
|
AccessApprovalRequestTemplate.PreviewProps = {
|
||||||
|
requesterFullName: "Abigail Williams",
|
||||||
|
requesterEmail: "[email protected]",
|
||||||
|
isTemporary: true,
|
||||||
|
secretPath: "/api/secrets",
|
||||||
|
environment: "Production",
|
||||||
|
siteUrl: "https://infisical.com",
|
||||||
|
projectName: "Example Project",
|
||||||
|
expiresIn: "1 day",
|
||||||
|
permissions: ["Read Secret", "Delete Project", "Create Dynamic Secret"],
|
||||||
|
note: "I need access to these permissions for the new initiative for HR."
|
||||||
|
} as AccessApprovalRequestTemplateProps;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Body, Container, Head, Hr, Html, Img, Link, Preview, Section, Tailwind, Text } from "@react-email/components";
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
|
||||||
|
export interface BaseEmailWrapperProps {
|
||||||
|
title: string;
|
||||||
|
preview: string;
|
||||||
|
siteUrl: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BaseEmailWrapper = ({ title, preview, children, siteUrl }: BaseEmailWrapperProps) => {
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head title={title} />
|
||||||
|
<Tailwind>
|
||||||
|
<Body className="bg-gray-300 my-auto mx-auto font-sans px-[8px]">
|
||||||
|
<Preview>{preview}</Preview>
|
||||||
|
<Container className="bg-white rounded-xl my-[40px] mx-auto pb-[0px] max-w-[500px]">
|
||||||
|
<Section className="border-0 border-b border-[#d1e309] border-solid bg-[#EBF852] mb-[44px] h-[10px] rounded-t-xl" />
|
||||||
|
<Section className="px-[32px] mb-[18px]">
|
||||||
|
<Section className="w-[48px] h-[48px] border border-solid border-gray-300 rounded-full bg-gray-100 mx-auto">
|
||||||
|
<Img
|
||||||
|
src={`https://infisical.com/_next/image?url=%2Fimages%2Flogo-black.png&w=64&q=75`}
|
||||||
|
width="32"
|
||||||
|
alt="Infisical Logo"
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
</Section>
|
||||||
|
<Section className="px-[28px]">{children}</Section>
|
||||||
|
<Hr className=" mt-[32px] mb-[0px] h-[1px]" />
|
||||||
|
<Section className="px-[24px] text-center">
|
||||||
|
<Text className="text-gray-500 text-[12px]">
|
||||||
|
Email sent via{" "}
|
||||||
|
<Link href={siteUrl} className="text-slate-700 no-underline">
|
||||||
|
Infisical
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Heading, Link, Section, Text } from "@react-email/components";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface EmailMfaTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
code: string;
|
||||||
|
isCloud: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailMfaTemplate = ({ code, siteUrl, isCloud }: EmailMfaTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper title="MFA Code" preview="Sign-in attempt requires further verification." siteUrl={siteUrl}>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
<strong>MFA required</strong>
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[8px] text-center pb-[8px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text>Enter the MFA code shown below in the browser where you started sign-in.</Text>
|
||||||
|
<Text className="text-[24px] mt-[16px]">
|
||||||
|
<strong>{code}</strong>
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
|
||||||
|
<Text className="mb-[0px]">
|
||||||
|
<strong>Not you?</strong>{" "}
|
||||||
|
{isCloud ? (
|
||||||
|
<>
|
||||||
|
Contact us at{" "}
|
||||||
|
<Link href="mailto:[email protected]" className="text-slate-700 no-underline">
|
||||||
|
support@infisical.com
|
||||||
|
</Link>{" "}
|
||||||
|
immediately
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Contact your administrator immediately"
|
||||||
|
)}
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailMfaTemplate;
|
||||||
|
|
||||||
|
EmailMfaTemplate.PreviewProps = {
|
||||||
|
code: "124356",
|
||||||
|
isCloud: true,
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as EmailMfaTemplateProps;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user