mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-24 20:43:19 +00:00
Compare commits
106 Commits
audit-log-
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
632572f7c3 | ||
|
a524690d01 | ||
|
f93edbb37f | ||
|
fa8154ecdd | ||
|
d977092502 | ||
|
cceb29b93a | ||
|
02b44365f1 | ||
|
b506393765 | ||
|
204269a10d | ||
|
cf1f83aaa3 | ||
|
7894181234 | ||
|
0c214a2f26 | ||
|
f5862cbb9a | ||
|
bb699ecb5f | ||
|
04b20ed11d | ||
|
cd1e2af9bf | ||
|
7a4a877e39 | ||
|
8f670bde88 | ||
|
ff9011c899 | ||
|
57c96abe03 | ||
|
178acc412d | ||
|
b0288c49c0 | ||
|
f5bb0d4a86 | ||
|
7699705334 | ||
|
7c49f6e302 | ||
|
b329b5aa4b | ||
|
0882c181d0 | ||
|
8672dd641a | ||
|
c613bb642e | ||
|
90fdba0b77 | ||
|
795ce11062 | ||
|
2d4adfc651 | ||
|
cb826f1a77 | ||
|
55f6a06440 | ||
|
a19e5ff905 | ||
|
dccada8a12 | ||
|
68bbff455f | ||
|
fcb59a1482 | ||
|
b92bc2183a | ||
|
aff318cf3c | ||
|
c97a3f07a7 | ||
|
e0dc2dd6d8 | ||
|
8bf5b0f457 | ||
|
4973447676 | ||
|
bd2e2b7931 | ||
|
13b7729af8 | ||
|
e25c1199bc | ||
|
6b3726957a | ||
|
c64e6310a6 | ||
|
aa893a40a9 | ||
|
0e488d840f | ||
|
d6186f1fe8 | ||
|
cd199f9d3e | ||
|
71258b6ea7 | ||
|
49c90c801e | ||
|
d019011822 | ||
|
8bd21ffa63 | ||
|
024a1891d3 | ||
|
ac7ac79463 | ||
|
23df78eff8 | ||
|
84255d1b26 | ||
|
3a6b2a593b | ||
|
d3ee30f5e6 | ||
|
317b15157d | ||
|
f145a00ef5 | ||
|
2e34167a24 | ||
|
0fc7d04455 | ||
|
af12518f54 | ||
|
cc193b9a9f | ||
|
0e95600db3 | ||
|
b60172f2be | ||
|
33dea34061 | ||
|
bc1cce62ab | ||
|
da68073e86 | ||
|
7bd312a287 | ||
|
d61e6752d6 | ||
|
636aee2ea9 | ||
|
5819b8c576 | ||
|
a838f84601 | ||
|
a32b590dc5 | ||
|
b330fdbc58 | ||
|
b85809293c | ||
|
f143d8c358 | ||
|
2e3330bf69 | ||
|
778d6b9bbf | ||
|
ddd46acbde | ||
|
e6165f7790 | ||
|
ac12f9fc66 | ||
|
7408d38065 | ||
|
e0c458df4b | ||
|
6a751e720c | ||
|
9032bbe514 | ||
|
1ea8e5a81e | ||
|
39ff7fddee | ||
|
a0014230f9 | ||
|
60d0bc827c | ||
|
6e9651d188 | ||
|
42aa3c3d46 | ||
|
184d353de5 | ||
|
b2360f9cc8 | ||
|
846a5a6e19 | ||
|
c6cd3a8cc0 | ||
|
796f5510ca | ||
|
0265665e83 | ||
|
79e425d807 | ||
|
c1570930a9 |
@@ -0,0 +1,47 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { ProjectType, TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasDefaultUserCaCol = await knex.schema.hasColumn(TableName.ProjectSshConfig, "defaultUserSshCaId");
|
||||||
|
const hasDefaultHostCaCol = await knex.schema.hasColumn(TableName.ProjectSshConfig, "defaultHostSshCaId");
|
||||||
|
|
||||||
|
if (hasDefaultUserCaCol && hasDefaultHostCaCol) {
|
||||||
|
await knex.schema.alterTable(TableName.ProjectSshConfig, (t) => {
|
||||||
|
t.dropForeign(["defaultUserSshCaId"]);
|
||||||
|
t.dropForeign(["defaultHostSshCaId"]);
|
||||||
|
});
|
||||||
|
await knex.schema.alterTable(TableName.ProjectSshConfig, (t) => {
|
||||||
|
// allow nullable (does not wipe existing values)
|
||||||
|
t.uuid("defaultUserSshCaId").nullable().alter();
|
||||||
|
t.uuid("defaultHostSshCaId").nullable().alter();
|
||||||
|
// re-add with SET NULL behavior (previously CASCADE)
|
||||||
|
t.foreign("defaultUserSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL");
|
||||||
|
t.foreign("defaultHostSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// (dangtony98): backfill by adding null defaults CAs for all existing Infisical SSH projects
|
||||||
|
// that do not have an associated ProjectSshConfig record introduced in Infisical SSH V2.
|
||||||
|
|
||||||
|
const allProjects = await knex(TableName.Project).where("type", ProjectType.SSH).select("id");
|
||||||
|
|
||||||
|
const projectsWithConfig = await knex(TableName.ProjectSshConfig).select("projectId");
|
||||||
|
const projectIdsWithConfig = new Set(projectsWithConfig.map((config) => config.projectId));
|
||||||
|
|
||||||
|
const projectsNeedingConfig = allProjects.filter((project) => !projectIdsWithConfig.has(project.id));
|
||||||
|
|
||||||
|
if (projectsNeedingConfig.length > 0) {
|
||||||
|
const configsToInsert = projectsNeedingConfig.map((project) => ({
|
||||||
|
projectId: project.id,
|
||||||
|
defaultUserSshCaId: null,
|
||||||
|
defaultHostSshCaId: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
}));
|
||||||
|
|
||||||
|
await knex.batchInsert(TableName.ProjectSshConfig, configsToInsert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(): Promise<void> {}
|
@@ -0,0 +1,19 @@
|
|||||||
|
import {
|
||||||
|
AwsIamUserSecretRotationGeneratedCredentialsSchema,
|
||||||
|
AwsIamUserSecretRotationSchema,
|
||||||
|
CreateAwsIamUserSecretRotationSchema,
|
||||||
|
UpdateAwsIamUserSecretRotationSchema
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
|
||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
|
||||||
|
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||||
|
|
||||||
|
export const registerAwsIamUserSecretRotationRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSecretRotationEndpoints({
|
||||||
|
type: SecretRotation.AwsIamUserSecret,
|
||||||
|
server,
|
||||||
|
responseSchema: AwsIamUserSecretRotationSchema,
|
||||||
|
createSchema: CreateAwsIamUserSecretRotationSchema,
|
||||||
|
updateSchema: UpdateAwsIamUserSecretRotationSchema,
|
||||||
|
generatedCredentialsSchema: AwsIamUserSecretRotationGeneratedCredentialsSchema
|
||||||
|
});
|
@@ -1,6 +1,8 @@
|
|||||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
|
||||||
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
|
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
|
||||||
|
import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router";
|
||||||
|
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
|
||||||
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
|
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
|
||||||
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
||||||
|
|
||||||
@@ -12,5 +14,7 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
|
|||||||
> = {
|
> = {
|
||||||
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
|
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
|
||||||
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
|
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
|
||||||
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter
|
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
|
||||||
|
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
|
||||||
|
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter
|
||||||
};
|
};
|
||||||
|
@@ -0,0 +1,19 @@
|
|||||||
|
import {
|
||||||
|
CreateLdapPasswordRotationSchema,
|
||||||
|
LdapPasswordRotationGeneratedCredentialsSchema,
|
||||||
|
LdapPasswordRotationSchema,
|
||||||
|
UpdateLdapPasswordRotationSchema
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
|
||||||
|
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||||
|
|
||||||
|
export const registerLdapPasswordRotationRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSecretRotationEndpoints({
|
||||||
|
type: SecretRotation.LdapPassword,
|
||||||
|
server,
|
||||||
|
responseSchema: LdapPasswordRotationSchema,
|
||||||
|
createSchema: CreateLdapPasswordRotationSchema,
|
||||||
|
updateSchema: UpdateLdapPasswordRotationSchema,
|
||||||
|
generatedCredentialsSchema: LdapPasswordRotationGeneratedCredentialsSchema
|
||||||
|
});
|
@@ -2,6 +2,8 @@ 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 { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||||
|
import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
|
||||||
|
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||||
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||||
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||||
@@ -13,7 +15,9 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
|||||||
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
|
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
|
||||||
PostgresCredentialsRotationListItemSchema,
|
PostgresCredentialsRotationListItemSchema,
|
||||||
MsSqlCredentialsRotationListItemSchema,
|
MsSqlCredentialsRotationListItemSchema,
|
||||||
Auth0ClientSecretRotationListItemSchema
|
Auth0ClientSecretRotationListItemSchema,
|
||||||
|
LdapPasswordRotationListItemSchema,
|
||||||
|
AwsIamUserSecretRotationListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
||||||
|
@@ -234,6 +234,7 @@ export enum EventType {
|
|||||||
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
|
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
|
||||||
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
|
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
|
||||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
|
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
|
||||||
|
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
|
||||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||||
@@ -248,6 +249,8 @@ export enum EventType {
|
|||||||
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
||||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||||
|
GET_PROJECT_SSH_CONFIG = "get-project-ssh-config",
|
||||||
|
UPDATE_PROJECT_SSH_CONFIG = "update-project-ssh-config",
|
||||||
INTEGRATION_SYNCED = "integration-synced",
|
INTEGRATION_SYNCED = "integration-synced",
|
||||||
CREATE_CMEK = "create-cmek",
|
CREATE_CMEK = "create-cmek",
|
||||||
UPDATE_CMEK = "update-cmek",
|
UPDATE_CMEK = "update-cmek",
|
||||||
@@ -1907,6 +1910,11 @@ interface OrgAdminAccessProjectEvent {
|
|||||||
}; // no metadata yet
|
}; // no metadata yet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OrgAdminBypassSSOEvent {
|
||||||
|
type: EventType.ORG_ADMIN_BYPASS_SSO;
|
||||||
|
metadata: Record<string, string>; // no metadata yet
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateCertificateTemplateEstConfig {
|
interface CreateCertificateTemplateEstConfig {
|
||||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -1986,6 +1994,25 @@ interface GetProjectSlackConfig {
|
|||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetProjectSshConfig {
|
||||||
|
type: EventType.GET_PROJECT_SSH_CONFIG;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateProjectSshConfig {
|
||||||
|
type: EventType.UPDATE_PROJECT_SSH_CONFIG;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
defaultUserSshCaId?: string | null;
|
||||||
|
defaultHostSshCaId?: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface IntegrationSyncedEvent {
|
interface IntegrationSyncedEvent {
|
||||||
type: EventType.INTEGRATION_SYNCED;
|
type: EventType.INTEGRATION_SYNCED;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2656,6 +2683,7 @@ export type Event =
|
|||||||
| GetProjectKmsBackupEvent
|
| GetProjectKmsBackupEvent
|
||||||
| LoadProjectKmsBackupEvent
|
| LoadProjectKmsBackupEvent
|
||||||
| OrgAdminAccessProjectEvent
|
| OrgAdminAccessProjectEvent
|
||||||
|
| OrgAdminBypassSSOEvent
|
||||||
| CreateCertificateTemplate
|
| CreateCertificateTemplate
|
||||||
| UpdateCertificateTemplate
|
| UpdateCertificateTemplate
|
||||||
| GetCertificateTemplate
|
| GetCertificateTemplate
|
||||||
@@ -2670,6 +2698,8 @@ export type Event =
|
|||||||
| GetSlackIntegration
|
| GetSlackIntegration
|
||||||
| UpdateProjectSlackConfig
|
| UpdateProjectSlackConfig
|
||||||
| GetProjectSlackConfig
|
| GetProjectSlackConfig
|
||||||
|
| GetProjectSshConfig
|
||||||
|
| UpdateProjectSshConfig
|
||||||
| IntegrationSyncedEvent
|
| IntegrationSyncedEvent
|
||||||
| CreateCmekEvent
|
| CreateCmekEvent
|
||||||
| UpdateCmekEvent
|
| UpdateCmekEvent
|
||||||
|
@@ -130,7 +130,17 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
if (expireAt > maxExpiryDate) throw new BadRequestError({ message: "TTL cannot be larger than max TTL" });
|
if (expireAt > maxExpiryDate) throw new BadRequestError({ message: "TTL cannot be larger than max TTL" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { entityId, data } = await selectedProvider.create(decryptedStoredInput, expireAt.getTime());
|
let result;
|
||||||
|
try {
|
||||||
|
result = await selectedProvider.create(decryptedStoredInput, expireAt.getTime());
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
|
||||||
|
throw new BadRequestError({ message: error.sqlMessage as string });
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const { entityId, data } = result;
|
||||||
|
|
||||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.create({
|
const dynamicSecretLease = await dynamicSecretLeaseDAL.create({
|
||||||
expireAt,
|
expireAt,
|
||||||
version: 1,
|
version: 1,
|
||||||
|
@@ -965,7 +965,6 @@ const buildMemberPermissionRules = () => {
|
|||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
||||||
|
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateAuthorities);
|
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
|
||||||
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||||
@@ -1031,7 +1030,6 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||||
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||||
|
@@ -267,7 +267,6 @@ export const secretReplicationServiceFactory = ({
|
|||||||
const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared });
|
const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared });
|
||||||
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
|
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
|
||||||
const sourceImportedSecrets = await fnSecretsV2FromImports({
|
const sourceImportedSecrets = await fnSecretsV2FromImports({
|
||||||
projectId,
|
|
||||||
secretImports: sourceSecretImports,
|
secretImports: sourceSecretImports,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
export const AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||||
|
name: "AWS IAM User Secret",
|
||||||
|
type: SecretRotation.AwsIamUserSecret,
|
||||||
|
connection: AppConnection.AWS,
|
||||||
|
template: {
|
||||||
|
secretsMapping: {
|
||||||
|
accessKeyId: "AWS_ACCESS_KEY_ID",
|
||||||
|
secretAccessKey: "AWS_SECRET_ACCESS_KEY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,123 @@
|
|||||||
|
import AWS from "aws-sdk";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TAwsIamUserSecretRotationGeneratedCredentials,
|
||||||
|
TAwsIamUserSecretRotationWithConnection
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-types";
|
||||||
|
import {
|
||||||
|
TRotationFactory,
|
||||||
|
TRotationFactoryGetSecretsPayload,
|
||||||
|
TRotationFactoryIssueCredentials,
|
||||||
|
TRotationFactoryRevokeCredentials,
|
||||||
|
TRotationFactoryRotateCredentials
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
|
import { getAwsConnectionConfig } from "@app/services/app-connection/aws";
|
||||||
|
|
||||||
|
const getCreateDate = (key: AWS.IAM.AccessKeyMetadata): number => {
|
||||||
|
return key.CreateDate ? new Date(key.CreateDate).getTime() : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const awsIamUserSecretRotationFactory: TRotationFactory<
|
||||||
|
TAwsIamUserSecretRotationWithConnection,
|
||||||
|
TAwsIamUserSecretRotationGeneratedCredentials
|
||||||
|
> = (secretRotation) => {
|
||||||
|
const {
|
||||||
|
parameters: { region, userName },
|
||||||
|
connection,
|
||||||
|
secretsMapping
|
||||||
|
} = secretRotation;
|
||||||
|
|
||||||
|
const $rotateClientSecret = async () => {
|
||||||
|
const { credentials } = await getAwsConnectionConfig(connection, region);
|
||||||
|
const iam = new AWS.IAM({ credentials });
|
||||||
|
|
||||||
|
const { AccessKeyMetadata } = await iam.listAccessKeys({ UserName: userName }).promise();
|
||||||
|
|
||||||
|
if (AccessKeyMetadata && AccessKeyMetadata.length > 0) {
|
||||||
|
// Sort keys by creation date (oldest first)
|
||||||
|
const sortedKeys = [...AccessKeyMetadata].sort((a, b) => getCreateDate(a) - getCreateDate(b));
|
||||||
|
|
||||||
|
// If we already have 2 keys, delete the oldest one
|
||||||
|
if (sortedKeys.length >= 2) {
|
||||||
|
const accessId = sortedKeys[0].AccessKeyId || sortedKeys[1].AccessKeyId;
|
||||||
|
if (accessId) {
|
||||||
|
await iam
|
||||||
|
.deleteAccessKey({
|
||||||
|
UserName: userName,
|
||||||
|
AccessKeyId: accessId
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { AccessKey } = await iam.createAccessKey({ UserName: userName }).promise();
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessKeyId: AccessKey.AccessKeyId,
|
||||||
|
secretAccessKey: AccessKey.SecretAccessKey
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const issueCredentials: TRotationFactoryIssueCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
const credentials = await $rotateClientSecret();
|
||||||
|
|
||||||
|
return callback(credentials);
|
||||||
|
};
|
||||||
|
|
||||||
|
const revokeCredentials: TRotationFactoryRevokeCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
|
||||||
|
generatedCredentials,
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
const { credentials } = await getAwsConnectionConfig(connection, region);
|
||||||
|
const iam = new AWS.IAM({ credentials });
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
generatedCredentials.map((generatedCredential) =>
|
||||||
|
iam
|
||||||
|
.deleteAccessKey({
|
||||||
|
UserName: userName,
|
||||||
|
AccessKeyId: generatedCredential.accessKeyId
|
||||||
|
})
|
||||||
|
.promise()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const rotateCredentials: TRotationFactoryRotateCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
|
||||||
|
_,
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
const credentials = await $rotateClientSecret();
|
||||||
|
|
||||||
|
return callback(credentials);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TAwsIamUserSecretRotationGeneratedCredentials> = (
|
||||||
|
generatedCredentials
|
||||||
|
) => {
|
||||||
|
const secrets = [
|
||||||
|
{
|
||||||
|
key: secretsMapping.accessKeyId,
|
||||||
|
value: generatedCredentials.accessKeyId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: secretsMapping.secretAccessKey,
|
||||||
|
value: generatedCredentials.secretAccessKey
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
issueCredentials,
|
||||||
|
revokeCredentials,
|
||||||
|
rotateCredentials,
|
||||||
|
getSecretsPayload
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,68 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import {
|
||||||
|
BaseCreateSecretRotationSchema,
|
||||||
|
BaseSecretRotationSchema,
|
||||||
|
BaseUpdateSecretRotationSchema
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||||
|
import { SecretRotations } from "@app/lib/api-docs";
|
||||||
|
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||||
|
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
export const AwsIamUserSecretRotationGeneratedCredentialsSchema = z
|
||||||
|
.object({
|
||||||
|
accessKeyId: z.string(),
|
||||||
|
secretAccessKey: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.max(2);
|
||||||
|
|
||||||
|
const AwsIamUserSecretRotationParametersSchema = z.object({
|
||||||
|
userName: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Client Name Required")
|
||||||
|
.describe(SecretRotations.PARAMETERS.AWS_IAM_USER_SECRET.userName),
|
||||||
|
region: z.nativeEnum(AWSRegion).describe(SecretRotations.PARAMETERS.AWS_IAM_USER_SECRET.region).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const AwsIamUserSecretRotationSecretsMappingSchema = z.object({
|
||||||
|
accessKeyId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AWS_IAM_USER_SECRET.accessKeyId),
|
||||||
|
secretAccessKey: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AWS_IAM_USER_SECRET.secretAccessKey)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AwsIamUserSecretRotationTemplateSchema = z.object({
|
||||||
|
secretsMapping: z.object({
|
||||||
|
accessKeyId: z.string(),
|
||||||
|
secretAccessKey: z.string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AwsIamUserSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.AwsIamUserSecret).extend({
|
||||||
|
type: z.literal(SecretRotation.AwsIamUserSecret),
|
||||||
|
parameters: AwsIamUserSecretRotationParametersSchema,
|
||||||
|
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateAwsIamUserSecretRotationSchema = BaseCreateSecretRotationSchema(
|
||||||
|
SecretRotation.AwsIamUserSecret
|
||||||
|
).extend({
|
||||||
|
parameters: AwsIamUserSecretRotationParametersSchema,
|
||||||
|
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateAwsIamUserSecretRotationSchema = BaseUpdateSecretRotationSchema(
|
||||||
|
SecretRotation.AwsIamUserSecret
|
||||||
|
).extend({
|
||||||
|
parameters: AwsIamUserSecretRotationParametersSchema.optional(),
|
||||||
|
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AwsIamUserSecretRotationListItemSchema = z.object({
|
||||||
|
name: z.literal("AWS IAM User Secret"),
|
||||||
|
connection: z.literal(AppConnection.AWS),
|
||||||
|
type: z.literal(SecretRotation.AwsIamUserSecret),
|
||||||
|
template: AwsIamUserSecretRotationTemplateSchema
|
||||||
|
});
|
@@ -0,0 +1,24 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TAwsConnection } from "@app/services/app-connection/aws";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AwsIamUserSecretRotationGeneratedCredentialsSchema,
|
||||||
|
AwsIamUserSecretRotationListItemSchema,
|
||||||
|
AwsIamUserSecretRotationSchema,
|
||||||
|
CreateAwsIamUserSecretRotationSchema
|
||||||
|
} from "./aws-iam-user-secret-rotation-schemas";
|
||||||
|
|
||||||
|
export type TAwsIamUserSecretRotation = z.infer<typeof AwsIamUserSecretRotationSchema>;
|
||||||
|
|
||||||
|
export type TAwsIamUserSecretRotationInput = z.infer<typeof CreateAwsIamUserSecretRotationSchema>;
|
||||||
|
|
||||||
|
export type TAwsIamUserSecretRotationListItem = z.infer<typeof AwsIamUserSecretRotationListItemSchema>;
|
||||||
|
|
||||||
|
export type TAwsIamUserSecretRotationWithConnection = TAwsIamUserSecretRotation & {
|
||||||
|
connection: TAwsConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAwsIamUserSecretRotationGeneratedCredentials = z.infer<
|
||||||
|
typeof AwsIamUserSecretRotationGeneratedCredentialsSchema
|
||||||
|
>;
|
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./aws-iam-user-secret-rotation-constants";
|
||||||
|
export * from "./aws-iam-user-secret-rotation-schemas";
|
||||||
|
export * from "./aws-iam-user-secret-rotation-types";
|
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./ldap-password-rotation-constants";
|
||||||
|
export * from "./ldap-password-rotation-schemas";
|
||||||
|
export * from "./ldap-password-rotation-types";
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
export const LDAP_PASSWORD_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||||
|
name: "LDAP Password",
|
||||||
|
type: SecretRotation.LdapPassword,
|
||||||
|
connection: AppConnection.LDAP,
|
||||||
|
template: {
|
||||||
|
secretsMapping: {
|
||||||
|
dn: "LDAP_DN",
|
||||||
|
password: "LDAP_PASSWORD"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,181 @@
|
|||||||
|
import ldap from "ldapjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TRotationFactory,
|
||||||
|
TRotationFactoryGetSecretsPayload,
|
||||||
|
TRotationFactoryIssueCredentials,
|
||||||
|
TRotationFactoryRevokeCredentials,
|
||||||
|
TRotationFactoryRotateCredentials
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||||
|
import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap";
|
||||||
|
|
||||||
|
import { generatePassword } from "../shared/utils";
|
||||||
|
import {
|
||||||
|
TLdapPasswordRotationGeneratedCredentials,
|
||||||
|
TLdapPasswordRotationWithConnection
|
||||||
|
} from "./ldap-password-rotation-types";
|
||||||
|
|
||||||
|
const getEncodedPassword = (password: string) => Buffer.from(`"${password}"`, "utf16le");
|
||||||
|
|
||||||
|
export const ldapPasswordRotationFactory: TRotationFactory<
|
||||||
|
TLdapPasswordRotationWithConnection,
|
||||||
|
TLdapPasswordRotationGeneratedCredentials
|
||||||
|
> = (secretRotation, appConnectionDAL, kmsService) => {
|
||||||
|
const {
|
||||||
|
connection,
|
||||||
|
parameters: { dn, passwordRequirements },
|
||||||
|
secretsMapping
|
||||||
|
} = secretRotation;
|
||||||
|
|
||||||
|
const $verifyCredentials = async (credentials: Pick<TLdapConnection["credentials"], "dn" | "password">) => {
|
||||||
|
try {
|
||||||
|
const client = await getLdapConnectionClient({ ...connection.credentials, ...credentials });
|
||||||
|
|
||||||
|
client.unbind();
|
||||||
|
client.destroy();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to verify credentials - ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $rotatePassword = async () => {
|
||||||
|
const { credentials, orgId } = connection;
|
||||||
|
|
||||||
|
if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection");
|
||||||
|
|
||||||
|
const client = await getLdapConnectionClient(credentials);
|
||||||
|
const isPersonalRotation = credentials.dn === dn;
|
||||||
|
|
||||||
|
const password = generatePassword(passwordRequirements);
|
||||||
|
|
||||||
|
let changes: ldap.Change[] | ldap.Change;
|
||||||
|
|
||||||
|
switch (credentials.provider) {
|
||||||
|
case LdapProvider.ActiveDirectory:
|
||||||
|
{
|
||||||
|
const encodedPassword = getEncodedPassword(password);
|
||||||
|
|
||||||
|
// service account vs personal password rotation require different changes
|
||||||
|
if (isPersonalRotation) {
|
||||||
|
const currentEncodedPassword = getEncodedPassword(credentials.password);
|
||||||
|
|
||||||
|
changes = [
|
||||||
|
new ldap.Change({
|
||||||
|
operation: "delete",
|
||||||
|
modification: {
|
||||||
|
type: "unicodePwd",
|
||||||
|
values: [currentEncodedPassword]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new ldap.Change({
|
||||||
|
operation: "add",
|
||||||
|
modification: {
|
||||||
|
type: "unicodePwd",
|
||||||
|
values: [encodedPassword]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
changes = new ldap.Change({
|
||||||
|
operation: "replace",
|
||||||
|
modification: {
|
||||||
|
type: "unicodePwd",
|
||||||
|
values: [encodedPassword]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled provider: ${credentials.provider as LdapProvider}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
client.modify(dn, changes, (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error(err, "LDAP Password Rotation Failed");
|
||||||
|
reject(new Error(`Provider Modify Error: ${err.message}`));
|
||||||
|
} else {
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
client.unbind();
|
||||||
|
client.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
await $verifyCredentials({ dn, password });
|
||||||
|
|
||||||
|
if (isPersonalRotation) {
|
||||||
|
const updatedCredentials: TLdapConnection["credentials"] = {
|
||||||
|
...credentials,
|
||||||
|
password
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: updatedCredentials,
|
||||||
|
orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
await appConnectionDAL.updateById(connection.id, { encryptedCredentials });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dn, password };
|
||||||
|
};
|
||||||
|
|
||||||
|
const issueCredentials: TRotationFactoryIssueCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
const credentials = await $rotatePassword();
|
||||||
|
|
||||||
|
return callback(credentials);
|
||||||
|
};
|
||||||
|
|
||||||
|
const revokeCredentials: TRotationFactoryRevokeCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
|
||||||
|
_,
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
// we just rotate to a new password, essentially revoking old credentials
|
||||||
|
await $rotatePassword();
|
||||||
|
|
||||||
|
return callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const rotateCredentials: TRotationFactoryRotateCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
|
||||||
|
_,
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
const credentials = await $rotatePassword();
|
||||||
|
|
||||||
|
return callback(credentials);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TLdapPasswordRotationGeneratedCredentials> = (
|
||||||
|
generatedCredentials
|
||||||
|
) => {
|
||||||
|
const secrets = [
|
||||||
|
{
|
||||||
|
key: secretsMapping.dn,
|
||||||
|
value: generatedCredentials.dn
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: secretsMapping.password,
|
||||||
|
value: generatedCredentials.password
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
issueCredentials,
|
||||||
|
revokeCredentials,
|
||||||
|
rotateCredentials,
|
||||||
|
getSecretsPayload
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,68 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import {
|
||||||
|
BaseCreateSecretRotationSchema,
|
||||||
|
BaseSecretRotationSchema,
|
||||||
|
BaseUpdateSecretRotationSchema
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||||
|
import { PasswordRequirementsSchema } from "@app/ee/services/secret-rotation-v2/shared/general";
|
||||||
|
import { SecretRotations } from "@app/lib/api-docs";
|
||||||
|
import { DistinguishedNameRegex } from "@app/lib/regex";
|
||||||
|
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
export const LdapPasswordRotationGeneratedCredentialsSchema = z
|
||||||
|
.object({
|
||||||
|
dn: z.string(),
|
||||||
|
password: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.max(2);
|
||||||
|
|
||||||
|
const LdapPasswordRotationParametersSchema = z.object({
|
||||||
|
dn: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com")
|
||||||
|
.min(1, "Distinguished Name (DN) Required")
|
||||||
|
.describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.dn),
|
||||||
|
passwordRequirements: PasswordRequirementsSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const LdapPasswordRotationSecretsMappingSchema = z.object({
|
||||||
|
dn: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.dn),
|
||||||
|
password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.password)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LdapPasswordRotationTemplateSchema = z.object({
|
||||||
|
secretsMapping: z.object({
|
||||||
|
dn: z.string(),
|
||||||
|
password: z.string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LdapPasswordRotationSchema = BaseSecretRotationSchema(SecretRotation.LdapPassword).extend({
|
||||||
|
type: z.literal(SecretRotation.LdapPassword),
|
||||||
|
parameters: LdapPasswordRotationParametersSchema,
|
||||||
|
secretsMapping: LdapPasswordRotationSecretsMappingSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword).extend({
|
||||||
|
parameters: LdapPasswordRotationParametersSchema,
|
||||||
|
secretsMapping: LdapPasswordRotationSecretsMappingSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateLdapPasswordRotationSchema = BaseUpdateSecretRotationSchema(SecretRotation.LdapPassword).extend({
|
||||||
|
parameters: LdapPasswordRotationParametersSchema.optional(),
|
||||||
|
secretsMapping: LdapPasswordRotationSecretsMappingSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LdapPasswordRotationListItemSchema = z.object({
|
||||||
|
name: z.literal("LDAP Password"),
|
||||||
|
connection: z.literal(AppConnection.LDAP),
|
||||||
|
type: z.literal(SecretRotation.LdapPassword),
|
||||||
|
template: LdapPasswordRotationTemplateSchema
|
||||||
|
});
|
@@ -0,0 +1,22 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TLdapConnection } from "@app/services/app-connection/ldap";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateLdapPasswordRotationSchema,
|
||||||
|
LdapPasswordRotationGeneratedCredentialsSchema,
|
||||||
|
LdapPasswordRotationListItemSchema,
|
||||||
|
LdapPasswordRotationSchema
|
||||||
|
} from "./ldap-password-rotation-schemas";
|
||||||
|
|
||||||
|
export type TLdapPasswordRotation = z.infer<typeof LdapPasswordRotationSchema>;
|
||||||
|
|
||||||
|
export type TLdapPasswordRotationInput = z.infer<typeof CreateLdapPasswordRotationSchema>;
|
||||||
|
|
||||||
|
export type TLdapPasswordRotationListItem = z.infer<typeof LdapPasswordRotationListItemSchema>;
|
||||||
|
|
||||||
|
export type TLdapPasswordRotationWithConnection = TLdapPasswordRotation & {
|
||||||
|
connection: TLdapConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TLdapPasswordRotationGeneratedCredentials = z.infer<typeof LdapPasswordRotationGeneratedCredentialsSchema>;
|
@@ -1,7 +1,9 @@
|
|||||||
export enum SecretRotation {
|
export enum SecretRotation {
|
||||||
PostgresCredentials = "postgres-credentials",
|
PostgresCredentials = "postgres-credentials",
|
||||||
MsSqlCredentials = "mssql-credentials",
|
MsSqlCredentials = "mssql-credentials",
|
||||||
Auth0ClientSecret = "auth0-client-secret"
|
Auth0ClientSecret = "auth0-client-secret",
|
||||||
|
LdapPassword = "ldap-password",
|
||||||
|
AwsIamUserSecret = "aws-iam-user-secret"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SecretRotationStatus {
|
export enum SecretRotationStatus {
|
||||||
|
@@ -4,6 +4,8 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
|
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
|
||||||
|
import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret";
|
||||||
|
import { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password";
|
||||||
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
|
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
|
||||||
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
||||||
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
|
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
|
||||||
@@ -18,7 +20,9 @@ import {
|
|||||||
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
|
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
|
||||||
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
|
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||||
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
|
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||||
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION
|
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
|
||||||
|
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
|
||||||
|
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listSecretRotationOptions = () => {
|
export const listSecretRotationOptions = () => {
|
||||||
|
@@ -3,12 +3,16 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
|||||||
|
|
||||||
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||||
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
||||||
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials",
|
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials",
|
||||||
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret"
|
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
|
||||||
|
[SecretRotation.LdapPassword]: "LDAP Password",
|
||||||
|
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||||
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
|
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
|
||||||
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0
|
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
|
||||||
|
[SecretRotation.LdapPassword]: AppConnection.LDAP,
|
||||||
|
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
|
||||||
};
|
};
|
||||||
|
@@ -14,6 +14,7 @@ import {
|
|||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns";
|
import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns";
|
||||||
|
import { ldapPasswordRotationFactory } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns";
|
||||||
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
import {
|
import {
|
||||||
calculateNextRotationAt,
|
calculateNextRotationAt,
|
||||||
@@ -77,6 +78,7 @@ 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 { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns";
|
||||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||||
|
|
||||||
export type TSecretRotationV2ServiceFactoryDep = {
|
export type TSecretRotationV2ServiceFactoryDep = {
|
||||||
@@ -114,7 +116,9 @@ type TRotationFactoryImplementation = TRotationFactory<
|
|||||||
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
|
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
|
||||||
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||||
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||||
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation
|
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
|
||||||
|
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
|
||||||
|
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation
|
||||||
};
|
};
|
||||||
|
|
||||||
export const secretRotationV2ServiceFactory = ({
|
export const secretRotationV2ServiceFactory = ({
|
||||||
@@ -449,6 +453,18 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
kmsService
|
kmsService
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// even though we have a db constraint we want to check before any rotation of credentials is attempted
|
||||||
|
// to prevent creation failure after external credentials have been modified
|
||||||
|
const conflictingRotation = await secretRotationV2DAL.findOne({
|
||||||
|
name: payload.name,
|
||||||
|
folderId: folder.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (conflictingRotation)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `A Secret Rotation with the name "${payload.name}" already exists at the secret path "${secretPath}"`
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
|
|
||||||
|
@@ -12,6 +12,20 @@ import {
|
|||||||
TAuth0ClientSecretRotationListItem,
|
TAuth0ClientSecretRotationListItem,
|
||||||
TAuth0ClientSecretRotationWithConnection
|
TAuth0ClientSecretRotationWithConnection
|
||||||
} from "./auth0-client-secret";
|
} from "./auth0-client-secret";
|
||||||
|
import {
|
||||||
|
TAwsIamUserSecretRotation,
|
||||||
|
TAwsIamUserSecretRotationGeneratedCredentials,
|
||||||
|
TAwsIamUserSecretRotationInput,
|
||||||
|
TAwsIamUserSecretRotationListItem,
|
||||||
|
TAwsIamUserSecretRotationWithConnection
|
||||||
|
} from "./aws-iam-user-secret";
|
||||||
|
import {
|
||||||
|
TLdapPasswordRotation,
|
||||||
|
TLdapPasswordRotationGeneratedCredentials,
|
||||||
|
TLdapPasswordRotationInput,
|
||||||
|
TLdapPasswordRotationListItem,
|
||||||
|
TLdapPasswordRotationWithConnection
|
||||||
|
} from "./ldap-password";
|
||||||
import {
|
import {
|
||||||
TMsSqlCredentialsRotation,
|
TMsSqlCredentialsRotation,
|
||||||
TMsSqlCredentialsRotationInput,
|
TMsSqlCredentialsRotationInput,
|
||||||
@@ -27,26 +41,39 @@ import {
|
|||||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||||
import { SecretRotation } from "./secret-rotation-v2-enums";
|
import { SecretRotation } from "./secret-rotation-v2-enums";
|
||||||
|
|
||||||
export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation;
|
export type TSecretRotationV2 =
|
||||||
|
| TPostgresCredentialsRotation
|
||||||
|
| TMsSqlCredentialsRotation
|
||||||
|
| TAuth0ClientSecretRotation
|
||||||
|
| TLdapPasswordRotation
|
||||||
|
| TAwsIamUserSecretRotation;
|
||||||
|
|
||||||
export type TSecretRotationV2WithConnection =
|
export type TSecretRotationV2WithConnection =
|
||||||
| TPostgresCredentialsRotationWithConnection
|
| TPostgresCredentialsRotationWithConnection
|
||||||
| TMsSqlCredentialsRotationWithConnection
|
| TMsSqlCredentialsRotationWithConnection
|
||||||
| TAuth0ClientSecretRotationWithConnection;
|
| TAuth0ClientSecretRotationWithConnection
|
||||||
|
| TLdapPasswordRotationWithConnection
|
||||||
|
| TAwsIamUserSecretRotationWithConnection;
|
||||||
|
|
||||||
export type TSecretRotationV2GeneratedCredentials =
|
export type TSecretRotationV2GeneratedCredentials =
|
||||||
| TSqlCredentialsRotationGeneratedCredentials
|
| TSqlCredentialsRotationGeneratedCredentials
|
||||||
| TAuth0ClientSecretRotationGeneratedCredentials;
|
| TAuth0ClientSecretRotationGeneratedCredentials
|
||||||
|
| TLdapPasswordRotationGeneratedCredentials
|
||||||
|
| TAwsIamUserSecretRotationGeneratedCredentials;
|
||||||
|
|
||||||
export type TSecretRotationV2Input =
|
export type TSecretRotationV2Input =
|
||||||
| TPostgresCredentialsRotationInput
|
| TPostgresCredentialsRotationInput
|
||||||
| TMsSqlCredentialsRotationInput
|
| TMsSqlCredentialsRotationInput
|
||||||
| TAuth0ClientSecretRotationInput;
|
| TAuth0ClientSecretRotationInput
|
||||||
|
| TLdapPasswordRotationInput
|
||||||
|
| TAwsIamUserSecretRotationInput;
|
||||||
|
|
||||||
export type TSecretRotationV2ListItem =
|
export type TSecretRotationV2ListItem =
|
||||||
| TPostgresCredentialsRotationListItem
|
| TPostgresCredentialsRotationListItem
|
||||||
| TMsSqlCredentialsRotationListItem
|
| TMsSqlCredentialsRotationListItem
|
||||||
| TAuth0ClientSecretRotationListItem;
|
| TAuth0ClientSecretRotationListItem
|
||||||
|
| TLdapPasswordRotationListItem
|
||||||
|
| TAwsIamUserSecretRotationListItem;
|
||||||
|
|
||||||
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
|
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
|
||||||
|
|
||||||
|
@@ -1,11 +1,16 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||||
|
import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||||
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||||
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||||
|
|
||||||
|
import { AwsIamUserSecretRotationSchema } from "./aws-iam-user-secret";
|
||||||
|
|
||||||
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
||||||
PostgresCredentialsRotationSchema,
|
PostgresCredentialsRotationSchema,
|
||||||
MsSqlCredentialsRotationSchema,
|
MsSqlCredentialsRotationSchema,
|
||||||
Auth0ClientSecretRotationSchema
|
Auth0ClientSecretRotationSchema,
|
||||||
|
LdapPasswordRotationSchema,
|
||||||
|
AwsIamUserSecretRotationSchema
|
||||||
]);
|
]);
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
export * from "./password-requirements-schema";
|
@@ -0,0 +1,44 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretRotations } from "@app/lib/api-docs";
|
||||||
|
|
||||||
|
export const PasswordRequirementsSchema = z
|
||||||
|
.object({
|
||||||
|
length: z
|
||||||
|
.number()
|
||||||
|
.min(1, "Password length must be a positive number")
|
||||||
|
.max(250, "Password length must be less than 250")
|
||||||
|
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.length),
|
||||||
|
required: z.object({
|
||||||
|
digits: z
|
||||||
|
.number()
|
||||||
|
.min(0, "Digit count must be non-negative")
|
||||||
|
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.digits),
|
||||||
|
lowercase: z
|
||||||
|
.number()
|
||||||
|
.min(0, "Lowercase count must be non-negative")
|
||||||
|
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.lowercase),
|
||||||
|
uppercase: z
|
||||||
|
.number()
|
||||||
|
.min(0, "Uppercase count must be non-negative")
|
||||||
|
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.uppercase),
|
||||||
|
symbols: z
|
||||||
|
.number()
|
||||||
|
.min(0, "Symbol count must be non-negative")
|
||||||
|
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.symbols)
|
||||||
|
}),
|
||||||
|
allowedSymbols: z
|
||||||
|
.string()
|
||||||
|
.regex(new RE2("[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?~]"), "Invalid symbols")
|
||||||
|
.optional()
|
||||||
|
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.allowedSymbols)
|
||||||
|
})
|
||||||
|
.refine((data) => {
|
||||||
|
return Object.values(data.required).some((count) => count > 0);
|
||||||
|
}, "At least one character type must be required")
|
||||||
|
.refine((data) => {
|
||||||
|
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
|
||||||
|
return total <= data.length;
|
||||||
|
}, "Sum of required characters cannot exceed the total length")
|
||||||
|
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.base);
|
@@ -1,6 +1,17 @@
|
|||||||
import { randomInt } from "crypto";
|
import { randomInt } from "crypto";
|
||||||
|
|
||||||
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
type TPasswordRequirements = {
|
||||||
|
length: number;
|
||||||
|
required: {
|
||||||
|
lowercase: number;
|
||||||
|
uppercase: number;
|
||||||
|
digits: number;
|
||||||
|
symbols: number;
|
||||||
|
};
|
||||||
|
allowedSymbols?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
|
||||||
length: 48,
|
length: 48,
|
||||||
required: {
|
required: {
|
||||||
lowercase: 1,
|
lowercase: 1,
|
||||||
@@ -11,9 +22,9 @@ const DEFAULT_PASSWORD_REQUIREMENTS = {
|
|||||||
allowedSymbols: "-_.~!*"
|
allowedSymbols: "-_.~!*"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generatePassword = () => {
|
export const generatePassword = (passwordRequirements?: TPasswordRequirements) => {
|
||||||
try {
|
try {
|
||||||
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
|
const { length, required, allowedSymbols } = passwordRequirements ?? DEFAULT_PASSWORD_REQUIREMENTS;
|
||||||
|
|
||||||
const chars = {
|
const chars = {
|
||||||
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||||
|
@@ -1857,6 +1857,20 @@ export const AppConnections = {
|
|||||||
WINDMILL: {
|
WINDMILL: {
|
||||||
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."
|
||||||
|
},
|
||||||
|
LDAP: {
|
||||||
|
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').",
|
||||||
|
dn: "The Distinguished Name (DN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com').",
|
||||||
|
password: "The password to bind with for authentication.",
|
||||||
|
sslRejectUnauthorized:
|
||||||
|
"Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.",
|
||||||
|
sslCertificate:
|
||||||
|
"The SSL certificate (PEM format) to use for secure connection when using ldaps:// with a self-signed certificate."
|
||||||
|
},
|
||||||
|
TEAMCITY: {
|
||||||
|
instanceUrl: "The TeamCity instance URL to connect with.",
|
||||||
|
accessToken: "The access token to use to connect with TeamCity."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1996,6 +2010,10 @@ export const SecretSyncs = {
|
|||||||
WINDMILL: {
|
WINDMILL: {
|
||||||
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."
|
||||||
|
},
|
||||||
|
TEAMCITY: {
|
||||||
|
project: "The TeamCity project to sync secrets to.",
|
||||||
|
buildConfig: "The TeamCity build configuration to sync secrets to."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2060,6 +2078,26 @@ export const SecretRotations = {
|
|||||||
},
|
},
|
||||||
AUTH0_CLIENT_SECRET: {
|
AUTH0_CLIENT_SECRET: {
|
||||||
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
|
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
|
||||||
|
},
|
||||||
|
LDAP_PASSWORD: {
|
||||||
|
dn: "The Distinguished Name (DN) of the principal to rotate the password for."
|
||||||
|
},
|
||||||
|
GENERAL: {
|
||||||
|
PASSWORD_REQUIREMENTS: {
|
||||||
|
base: "The password requirements to use when generating the new password.",
|
||||||
|
length: "The length of the password to generate.",
|
||||||
|
required: {
|
||||||
|
digits: "The amount of digits to require in the generated password.",
|
||||||
|
lowercase: "The amount of lowercase characters to require in the generated password.",
|
||||||
|
uppercase: "The amount of uppercase characters to require in the generated password.",
|
||||||
|
symbols: "The amount of symbols to require in the generated password."
|
||||||
|
},
|
||||||
|
allowedSymbols: 'The allowed symbols to use in the generated password (defaults to "-_.~!*").'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AWS_IAM_USER_SECRET: {
|
||||||
|
userName: "The name of the client to rotate credentials for.",
|
||||||
|
region: "The AWS region the client is present in."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SECRETS_MAPPING: {
|
SECRETS_MAPPING: {
|
||||||
@@ -2070,6 +2108,14 @@ export const SecretRotations = {
|
|||||||
AUTH0_CLIENT_SECRET: {
|
AUTH0_CLIENT_SECRET: {
|
||||||
clientId: "The name of the secret that the client ID will be mapped to.",
|
clientId: "The name of the secret that the client ID will be mapped to.",
|
||||||
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
|
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
|
||||||
|
},
|
||||||
|
LDAP_PASSWORD: {
|
||||||
|
dn: "The name of the secret that the Distinguished Name (DN) of the principal will be mapped to.",
|
||||||
|
password: "The name of the secret that the rotated password will be mapped to."
|
||||||
|
},
|
||||||
|
AWS_IAM_USER_SECRET: {
|
||||||
|
accessKeyId: "The name of the secret that the access key ID will be mapped to.",
|
||||||
|
secretAccessKey: "The name of the secret that the rotated secret access key will be mapped to."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
3
backend/src/lib/regex/index.ts
Normal file
3
backend/src/lib/regex/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const DistinguishedNameRegex =
|
||||||
|
// DN format, ie; CN=user,OU=users,DC=example,DC=com
|
||||||
|
/^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/;
|
@@ -596,7 +596,14 @@ export const registerRoutes = async (
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, totpService });
|
const loginService = authLoginServiceFactory({
|
||||||
|
userDAL,
|
||||||
|
smtpService,
|
||||||
|
tokenService,
|
||||||
|
orgDAL,
|
||||||
|
totpService,
|
||||||
|
auditLogService
|
||||||
|
});
|
||||||
const passwordService = authPaswordServiceFactory({
|
const passwordService = authPaswordServiceFactory({
|
||||||
tokenService,
|
tokenService,
|
||||||
smtpService,
|
smtpService,
|
||||||
@@ -1089,7 +1096,8 @@ export const registerRoutes = async (
|
|||||||
secretApprovalRequestSecretDAL,
|
secretApprovalRequestSecretDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
snapshotService,
|
snapshotService,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
keyStore
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretApprovalRequestService = secretApprovalRequestServiceFactory({
|
const secretApprovalRequestService = secretApprovalRequestServiceFactory({
|
||||||
|
@@ -28,11 +28,16 @@ import {
|
|||||||
HumanitecConnectionListItemSchema,
|
HumanitecConnectionListItemSchema,
|
||||||
SanitizedHumanitecConnectionSchema
|
SanitizedHumanitecConnectionSchema
|
||||||
} from "@app/services/app-connection/humanitec";
|
} from "@app/services/app-connection/humanitec";
|
||||||
|
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
|
||||||
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
|
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
|
||||||
import {
|
import {
|
||||||
PostgresConnectionListItemSchema,
|
PostgresConnectionListItemSchema,
|
||||||
SanitizedPostgresConnectionSchema
|
SanitizedPostgresConnectionSchema
|
||||||
} from "@app/services/app-connection/postgres";
|
} from "@app/services/app-connection/postgres";
|
||||||
|
import {
|
||||||
|
SanitizedTeamCityConnectionSchema,
|
||||||
|
TeamCityConnectionListItemSchema
|
||||||
|
} from "@app/services/app-connection/teamcity";
|
||||||
import {
|
import {
|
||||||
SanitizedTerraformCloudConnectionSchema,
|
SanitizedTerraformCloudConnectionSchema,
|
||||||
TerraformCloudConnectionListItemSchema
|
TerraformCloudConnectionListItemSchema
|
||||||
@@ -59,7 +64,9 @@ const SanitizedAppConnectionSchema = z.union([
|
|||||||
...SanitizedMsSqlConnectionSchema.options,
|
...SanitizedMsSqlConnectionSchema.options,
|
||||||
...SanitizedCamundaConnectionSchema.options,
|
...SanitizedCamundaConnectionSchema.options,
|
||||||
...SanitizedWindmillConnectionSchema.options,
|
...SanitizedWindmillConnectionSchema.options,
|
||||||
...SanitizedAuth0ConnectionSchema.options
|
...SanitizedAuth0ConnectionSchema.options,
|
||||||
|
...SanitizedLdapConnectionSchema.options,
|
||||||
|
...SanitizedTeamCityConnectionSchema.options
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||||
@@ -76,7 +83,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
|||||||
MsSqlConnectionListItemSchema,
|
MsSqlConnectionListItemSchema,
|
||||||
CamundaConnectionListItemSchema,
|
CamundaConnectionListItemSchema,
|
||||||
WindmillConnectionListItemSchema,
|
WindmillConnectionListItemSchema,
|
||||||
Auth0ConnectionListItemSchema
|
Auth0ConnectionListItemSchema,
|
||||||
|
LdapConnectionListItemSchema,
|
||||||
|
TeamCityConnectionListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
@@ -59,4 +59,40 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
|||||||
return { kmsKeys };
|
return { kmsKeys };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/users`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
iamUsers: z
|
||||||
|
.object({
|
||||||
|
UserName: z.string(),
|
||||||
|
Arn: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const iamUsers = await server.services.appConnection.aws.listIamUsers(
|
||||||
|
{
|
||||||
|
connectionId
|
||||||
|
},
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
return { iamUsers };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { registerAuth0ConnectionRouter } from "@app/server/routes/v1/app-connection-routers/auth0-connection-router";
|
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
|
||||||
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||||
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
|
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
|
||||||
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
|
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
|
||||||
@@ -9,8 +9,10 @@ import { registerDatabricksConnectionRouter } from "./databricks-connection-rout
|
|||||||
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 { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
||||||
|
import { registerLdapConnectionRouter } from "./ldap-connection-router";
|
||||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||||
|
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
|
||||||
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
|
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
|
||||||
import { registerVercelConnectionRouter } from "./vercel-connection-router";
|
import { registerVercelConnectionRouter } from "./vercel-connection-router";
|
||||||
import { registerWindmillConnectionRouter } from "./windmill-connection-router";
|
import { registerWindmillConnectionRouter } from "./windmill-connection-router";
|
||||||
@@ -32,5 +34,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
|||||||
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
|
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
|
||||||
[AppConnection.Camunda]: registerCamundaConnectionRouter,
|
[AppConnection.Camunda]: registerCamundaConnectionRouter,
|
||||||
[AppConnection.Windmill]: registerWindmillConnectionRouter,
|
[AppConnection.Windmill]: registerWindmillConnectionRouter,
|
||||||
[AppConnection.Auth0]: registerAuth0ConnectionRouter
|
[AppConnection.Auth0]: registerAuth0ConnectionRouter,
|
||||||
|
[AppConnection.LDAP]: registerLdapConnectionRouter,
|
||||||
|
[AppConnection.TeamCity]: registerTeamCityConnectionRouter
|
||||||
};
|
};
|
||||||
|
@@ -0,0 +1,18 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateLdapConnectionSchema,
|
||||||
|
SanitizedLdapConnectionSchema,
|
||||||
|
UpdateLdapConnectionSchema
|
||||||
|
} from "@app/services/app-connection/ldap";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerLdapConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.LDAP,
|
||||||
|
server,
|
||||||
|
sanitizedResponseSchema: SanitizedLdapConnectionSchema,
|
||||||
|
createSchema: CreateLdapConnectionSchema,
|
||||||
|
updateSchema: UpdateLdapConnectionSchema
|
||||||
|
});
|
||||||
|
};
|
@@ -0,0 +1,60 @@
|
|||||||
|
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 {
|
||||||
|
CreateTeamCityConnectionSchema,
|
||||||
|
SanitizedTeamCityConnectionSchema,
|
||||||
|
UpdateTeamCityConnectionSchema
|
||||||
|
} from "@app/services/app-connection/teamcity";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerTeamCityConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.TeamCity,
|
||||||
|
server,
|
||||||
|
sanitizedResponseSchema: SanitizedTeamCityConnectionSchema,
|
||||||
|
createSchema: CreateTeamCityConnectionSchema,
|
||||||
|
updateSchema: UpdateTeamCityConnectionSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
// The following endpoints are for internal Infisical App use only and not part of the public API
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/projects`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
buildTypes: z.object({
|
||||||
|
buildType: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
const projects = await server.services.appConnection.teamcity.listProjects(connectionId, req.permission);
|
||||||
|
|
||||||
|
return projects;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -1,3 +1,4 @@
|
|||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -6,6 +7,7 @@ import {
|
|||||||
ProjectMembershipsSchema,
|
ProjectMembershipsSchema,
|
||||||
ProjectRolesSchema,
|
ProjectRolesSchema,
|
||||||
ProjectSlackConfigsSchema,
|
ProjectSlackConfigsSchema,
|
||||||
|
ProjectSshConfigsSchema,
|
||||||
ProjectType,
|
ProjectType,
|
||||||
SecretFoldersSchema,
|
SecretFoldersSchema,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
@@ -78,7 +80,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
includeGroupMembers: z
|
includeGroupMembers: z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.default("false")
|
.default("false")
|
||||||
.transform((value) => value === "true")
|
.transform((value) => value === "true"),
|
||||||
|
roles: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.transform(decodeURIComponent)
|
||||||
|
.refine((value) => {
|
||||||
|
if (!value) return true;
|
||||||
|
const slugs = value.split(",");
|
||||||
|
return slugs.every((slug) => slugify(slug.trim(), { lowercase: true }) === slug.trim());
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim()
|
||||||
@@ -117,13 +129,15 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
const roles = (req.query.roles?.split(",") || []).filter(Boolean);
|
||||||
const users = await server.services.projectMembership.getProjectMemberships({
|
const users = await server.services.projectMembership.getProjectMemberships({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
includeGroupMembers: req.query.includeGroupMembers,
|
includeGroupMembers: req.query.includeGroupMembers,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
actorOrgId: req.permission.orgId
|
actorOrgId: req.permission.orgId,
|
||||||
|
roles
|
||||||
});
|
});
|
||||||
|
|
||||||
return { users };
|
return { users };
|
||||||
@@ -623,6 +637,107 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:workspaceId/ssh-config",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: ProjectSshConfigsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
projectId: true,
|
||||||
|
defaultUserSshCaId: true,
|
||||||
|
defaultHostSshCaId: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const sshConfig = await server.services.project.getProjectSshConfig({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: req.params.workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: sshConfig.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_PROJECT_SSH_CONFIG,
|
||||||
|
metadata: {
|
||||||
|
id: sshConfig.id,
|
||||||
|
projectId: sshConfig.projectId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return sshConfig;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:workspaceId/ssh-config",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
defaultUserSshCaId: z.string().optional(),
|
||||||
|
defaultHostSshCaId: z.string().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: ProjectSshConfigsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
projectId: true,
|
||||||
|
defaultUserSshCaId: true,
|
||||||
|
defaultHostSshCaId: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const sshConfig = await server.services.project.updateProjectSshConfig({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: sshConfig.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_PROJECT_SSH_CONFIG,
|
||||||
|
metadata: {
|
||||||
|
id: sshConfig.id,
|
||||||
|
projectId: sshConfig.projectId,
|
||||||
|
defaultUserSshCaId: sshConfig.defaultUserSshCaId,
|
||||||
|
defaultHostSshCaId: sshConfig.defaultHostSshCaId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return sshConfig;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/slack-config",
|
url: "/:workspaceId/slack-config",
|
||||||
|
@@ -9,6 +9,7 @@ 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 { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
||||||
|
import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
|
||||||
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
|
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
|
||||||
import { registerVercelSyncRouter } from "./vercel-sync-router";
|
import { registerVercelSyncRouter } from "./vercel-sync-router";
|
||||||
import { registerWindmillSyncRouter } from "./windmill-sync-router";
|
import { registerWindmillSyncRouter } from "./windmill-sync-router";
|
||||||
@@ -27,5 +28,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
|||||||
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
|
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
|
||||||
[SecretSync.Camunda]: registerCamundaSyncRouter,
|
[SecretSync.Camunda]: registerCamundaSyncRouter,
|
||||||
[SecretSync.Vercel]: registerVercelSyncRouter,
|
[SecretSync.Vercel]: registerVercelSyncRouter,
|
||||||
[SecretSync.Windmill]: registerWindmillSyncRouter
|
[SecretSync.Windmill]: registerWindmillSyncRouter,
|
||||||
|
[SecretSync.TeamCity]: registerTeamCitySyncRouter
|
||||||
};
|
};
|
||||||
|
@@ -23,6 +23,7 @@ import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/service
|
|||||||
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 { 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 { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
||||||
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
||||||
import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill";
|
import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill";
|
||||||
@@ -39,7 +40,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
|||||||
TerraformCloudSyncSchema,
|
TerraformCloudSyncSchema,
|
||||||
CamundaSyncSchema,
|
CamundaSyncSchema,
|
||||||
VercelSyncSchema,
|
VercelSyncSchema,
|
||||||
WindmillSyncSchema
|
WindmillSyncSchema,
|
||||||
|
TeamCitySyncSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||||
@@ -54,7 +56,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
|||||||
TerraformCloudSyncListItemSchema,
|
TerraformCloudSyncListItemSchema,
|
||||||
CamundaSyncListItemSchema,
|
CamundaSyncListItemSchema,
|
||||||
VercelSyncListItemSchema,
|
VercelSyncListItemSchema,
|
||||||
WindmillSyncListItemSchema
|
WindmillSyncListItemSchema,
|
||||||
|
TeamCitySyncListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import {
|
||||||
|
CreateTeamCitySyncSchema,
|
||||||
|
TeamCitySyncSchema,
|
||||||
|
UpdateTeamCitySyncSchema
|
||||||
|
} from "@app/services/secret-sync/teamcity";
|
||||||
|
|
||||||
|
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||||
|
|
||||||
|
export const registerTeamCitySyncRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSyncSecretsEndpoints({
|
||||||
|
destination: SecretSync.TeamCity,
|
||||||
|
server,
|
||||||
|
responseSchema: TeamCitySyncSchema,
|
||||||
|
createSchema: CreateTeamCitySyncSchema,
|
||||||
|
updateSchema: UpdateTeamCitySyncSchema
|
||||||
|
});
|
@@ -29,7 +29,8 @@ import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
|||||||
|
|
||||||
const projectWithEnv = SanitizedProjectSchema.extend({
|
const projectWithEnv = SanitizedProjectSchema.extend({
|
||||||
_id: z.string(),
|
_id: z.string(),
|
||||||
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
|
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array(),
|
||||||
|
kmsSecretManagerKeyId: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||||
|
@@ -252,6 +252,31 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/me/sessions/:sessionId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
sessionId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.authToken.revokeMySessionById(req.permission.id, req.params.sessionId);
|
||||||
|
return {
|
||||||
|
message: "Successfully revoked session"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/me",
|
url: "/me",
|
||||||
|
@@ -12,7 +12,9 @@ export enum AppConnection {
|
|||||||
MsSql = "mssql",
|
MsSql = "mssql",
|
||||||
Camunda = "camunda",
|
Camunda = "camunda",
|
||||||
Windmill = "windmill",
|
Windmill = "windmill",
|
||||||
Auth0 = "auth0"
|
Auth0 = "auth0",
|
||||||
|
LDAP = "ldap",
|
||||||
|
TeamCity = "teamcity"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AWSRegion {
|
export enum AWSRegion {
|
||||||
|
@@ -41,8 +41,14 @@ import {
|
|||||||
HumanitecConnectionMethod,
|
HumanitecConnectionMethod,
|
||||||
validateHumanitecConnectionCredentials
|
validateHumanitecConnectionCredentials
|
||||||
} from "./humanitec";
|
} from "./humanitec";
|
||||||
|
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
|
||||||
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
||||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||||
|
import {
|
||||||
|
getTeamCityConnectionListItem,
|
||||||
|
TeamCityConnectionMethod,
|
||||||
|
validateTeamCityConnectionCredentials
|
||||||
|
} from "./teamcity";
|
||||||
import {
|
import {
|
||||||
getTerraformCloudConnectionListItem,
|
getTerraformCloudConnectionListItem,
|
||||||
TerraformCloudConnectionMethod,
|
TerraformCloudConnectionMethod,
|
||||||
@@ -71,7 +77,9 @@ export const listAppConnectionOptions = () => {
|
|||||||
getMsSqlConnectionListItem(),
|
getMsSqlConnectionListItem(),
|
||||||
getCamundaConnectionListItem(),
|
getCamundaConnectionListItem(),
|
||||||
getWindmillConnectionListItem(),
|
getWindmillConnectionListItem(),
|
||||||
getAuth0ConnectionListItem()
|
getAuth0ConnectionListItem(),
|
||||||
|
getLdapConnectionListItem(),
|
||||||
|
getTeamCityConnectionListItem()
|
||||||
].sort((a, b) => a.name.localeCompare(b.name));
|
].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,7 +143,9 @@ export const validateAppConnectionCredentials = async (
|
|||||||
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[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.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator
|
||||||
};
|
};
|
||||||
|
|
||||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||||
@@ -167,9 +177,12 @@ 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 TeamCityConnectionMethod.AccessToken:
|
||||||
return "Access Token";
|
return "Access Token";
|
||||||
case Auth0ConnectionMethod.ClientCredentials:
|
case Auth0ConnectionMethod.ClientCredentials:
|
||||||
return "Client Credentials";
|
return "Client Credentials";
|
||||||
|
case LdapConnectionMethod.SimpleBind:
|
||||||
|
return "Simple Bind";
|
||||||
default:
|
default:
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
@@ -214,5 +227,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
|||||||
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
|
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
|
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
|
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
|
||||||
|
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
|
||||||
|
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
|
||||||
};
|
};
|
||||||
|
@@ -14,5 +14,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
|||||||
[AppConnection.MsSql]: "Microsoft SQL Server",
|
[AppConnection.MsSql]: "Microsoft SQL Server",
|
||||||
[AppConnection.Camunda]: "Camunda",
|
[AppConnection.Camunda]: "Camunda",
|
||||||
[AppConnection.Windmill]: "Windmill",
|
[AppConnection.Windmill]: "Windmill",
|
||||||
[AppConnection.Auth0]: "Auth0"
|
[AppConnection.Auth0]: "Auth0",
|
||||||
|
[AppConnection.LDAP]: "LDAP",
|
||||||
|
[AppConnection.TeamCity]: "TeamCity"
|
||||||
};
|
};
|
||||||
|
@@ -43,8 +43,11 @@ import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
|||||||
import { githubConnectionService } from "./github/github-connection-service";
|
import { githubConnectionService } from "./github/github-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 { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||||
|
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
|
||||||
|
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
|
||||||
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
|
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
|
||||||
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
|
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
|
||||||
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
|
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
|
||||||
@@ -74,7 +77,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
|||||||
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
||||||
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
|
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
|
||||||
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
|
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
|
||||||
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema
|
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
|
||||||
|
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
|
||||||
|
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appConnectionServiceFactory = ({
|
export const appConnectionServiceFactory = ({
|
||||||
@@ -450,6 +455,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
vercel: vercelConnectionService(connectAppConnectionById),
|
vercel: vercelConnectionService(connectAppConnectionById),
|
||||||
windmill: windmillConnectionService(connectAppConnectionById),
|
windmill: windmillConnectionService(connectAppConnectionById),
|
||||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
|
teamcity: teamcityConnectionService(connectAppConnectionById)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -57,12 +57,24 @@ import {
|
|||||||
THumanitecConnectionInput,
|
THumanitecConnectionInput,
|
||||||
TValidateHumanitecConnectionCredentialsSchema
|
TValidateHumanitecConnectionCredentialsSchema
|
||||||
} from "./humanitec";
|
} from "./humanitec";
|
||||||
|
import {
|
||||||
|
TLdapConnection,
|
||||||
|
TLdapConnectionConfig,
|
||||||
|
TLdapConnectionInput,
|
||||||
|
TValidateLdapConnectionCredentialsSchema
|
||||||
|
} from "./ldap";
|
||||||
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||||
import {
|
import {
|
||||||
TPostgresConnection,
|
TPostgresConnection,
|
||||||
TPostgresConnectionInput,
|
TPostgresConnectionInput,
|
||||||
TValidatePostgresConnectionCredentialsSchema
|
TValidatePostgresConnectionCredentialsSchema
|
||||||
} from "./postgres";
|
} from "./postgres";
|
||||||
|
import {
|
||||||
|
TTeamCityConnection,
|
||||||
|
TTeamCityConnectionConfig,
|
||||||
|
TTeamCityConnectionInput,
|
||||||
|
TValidateTeamCityConnectionCredentialsSchema
|
||||||
|
} from "./teamcity";
|
||||||
import {
|
import {
|
||||||
TTerraformCloudConnection,
|
TTerraformCloudConnection,
|
||||||
TTerraformCloudConnectionConfig,
|
TTerraformCloudConnectionConfig,
|
||||||
@@ -97,6 +109,8 @@ export type TAppConnection = { id: string } & (
|
|||||||
| TCamundaConnection
|
| TCamundaConnection
|
||||||
| TWindmillConnection
|
| TWindmillConnection
|
||||||
| TAuth0Connection
|
| TAuth0Connection
|
||||||
|
| TLdapConnection
|
||||||
|
| TTeamCityConnection
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||||
@@ -118,6 +132,8 @@ export type TAppConnectionInput = { id: string } & (
|
|||||||
| TCamundaConnectionInput
|
| TCamundaConnectionInput
|
||||||
| TWindmillConnectionInput
|
| TWindmillConnectionInput
|
||||||
| TAuth0ConnectionInput
|
| TAuth0ConnectionInput
|
||||||
|
| TLdapConnectionInput
|
||||||
|
| TTeamCityConnectionInput
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
|
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
|
||||||
@@ -144,7 +160,9 @@ export type TAppConnectionConfig =
|
|||||||
| TSqlConnectionConfig
|
| TSqlConnectionConfig
|
||||||
| TCamundaConnectionConfig
|
| TCamundaConnectionConfig
|
||||||
| TWindmillConnectionConfig
|
| TWindmillConnectionConfig
|
||||||
| TAuth0ConnectionConfig;
|
| TAuth0ConnectionConfig
|
||||||
|
| TLdapConnectionConfig
|
||||||
|
| TTeamCityConnectionConfig;
|
||||||
|
|
||||||
export type TValidateAppConnectionCredentialsSchema =
|
export type TValidateAppConnectionCredentialsSchema =
|
||||||
| TValidateAwsConnectionCredentialsSchema
|
| TValidateAwsConnectionCredentialsSchema
|
||||||
@@ -160,7 +178,9 @@ export type TValidateAppConnectionCredentialsSchema =
|
|||||||
| TValidateTerraformCloudConnectionCredentialsSchema
|
| TValidateTerraformCloudConnectionCredentialsSchema
|
||||||
| TValidateVercelConnectionCredentialsSchema
|
| TValidateVercelConnectionCredentialsSchema
|
||||||
| TValidateWindmillConnectionCredentialsSchema
|
| TValidateWindmillConnectionCredentialsSchema
|
||||||
| TValidateAuth0ConnectionCredentialsSchema;
|
| TValidateAuth0ConnectionCredentialsSchema
|
||||||
|
| TValidateLdapConnectionCredentialsSchema
|
||||||
|
| TValidateTeamCityConnectionCredentialsSchema;
|
||||||
|
|
||||||
export type TListAwsConnectionKmsKeys = {
|
export type TListAwsConnectionKmsKeys = {
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
@@ -168,6 +188,10 @@ export type TListAwsConnectionKmsKeys = {
|
|||||||
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TListAwsConnectionIamUsers = {
|
||||||
|
connectionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TAppConnectionCredentialsValidator = (
|
export type TAppConnectionCredentialsValidator = (
|
||||||
appConnection: TAppConnectionConfig
|
appConnection: TAppConnectionConfig
|
||||||
) => Promise<TAppConnection["credentials"]>;
|
) => Promise<TAppConnection["credentials"]>;
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||||
import AWS from "aws-sdk";
|
import AWS from "aws-sdk";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
import { AwsConnectionMethod } from "./aws-connection-enums";
|
import { AwsConnectionMethod } from "./aws-connection-enums";
|
||||||
@@ -90,9 +92,20 @@ export const validateAwsConnectionCredentials = async (appConnection: TAwsConnec
|
|||||||
const sts = new AWS.STS(awsConfig);
|
const sts = new AWS.STS(awsConfig);
|
||||||
|
|
||||||
resp = await sts.getCallerIdentity().promise();
|
resp = await sts.getCallerIdentity().promise();
|
||||||
} catch (e: unknown) {
|
} catch (error: unknown) {
|
||||||
|
logger.error(error, "Error validating AWS connection credentials");
|
||||||
|
|
||||||
|
let message: string;
|
||||||
|
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
message = (error.response?.data?.message as string) || error.message || "verify credentials";
|
||||||
|
} else {
|
||||||
|
message = (error as Error)?.message || "verify credentials";
|
||||||
|
}
|
||||||
|
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Unable to validate connection: verify credentials`
|
message: `Unable to validate connection: ${message}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,7 +2,10 @@ import AWS from "aws-sdk";
|
|||||||
|
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { TListAwsConnectionKmsKeys } from "@app/services/app-connection/app-connection-types";
|
import {
|
||||||
|
TListAwsConnectionIamUsers,
|
||||||
|
TListAwsConnectionKmsKeys
|
||||||
|
} from "@app/services/app-connection/app-connection-types";
|
||||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||||
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
@@ -70,6 +73,23 @@ const listAwsKmsKeys = async (
|
|||||||
return kmsKeys;
|
return kmsKeys;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listAwsIamUsers = async (appConnection: TAwsConnection) => {
|
||||||
|
const { credentials } = await getAwsConnectionConfig(appConnection);
|
||||||
|
|
||||||
|
const iam = new AWS.IAM({ credentials });
|
||||||
|
|
||||||
|
const userEntries: AWS.IAM.User[] = [];
|
||||||
|
let userMarker: string | undefined;
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const response = await iam.listUsers({ MaxItems: 100, Marker: userMarker }).promise();
|
||||||
|
userEntries.push(...(response.Users || []));
|
||||||
|
userMarker = response.Marker;
|
||||||
|
} while (userMarker);
|
||||||
|
|
||||||
|
return userEntries;
|
||||||
|
};
|
||||||
|
|
||||||
export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||||
const listKmsKeys = async (
|
const listKmsKeys = async (
|
||||||
{ connectionId, region, destination }: TListAwsConnectionKmsKeys,
|
{ connectionId, region, destination }: TListAwsConnectionKmsKeys,
|
||||||
@@ -82,7 +102,16 @@ export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) =>
|
|||||||
return kmsKeys;
|
return kmsKeys;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listIamUsers = async ({ connectionId }: TListAwsConnectionIamUsers, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.AWS, connectionId, actor);
|
||||||
|
|
||||||
|
const iamUsers = await listAwsIamUsers(appConnection);
|
||||||
|
|
||||||
|
return iamUsers;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
listKmsKeys
|
listKmsKeys,
|
||||||
|
listIamUsers
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
4
backend/src/services/app-connection/ldap/index.ts
Normal file
4
backend/src/services/app-connection/ldap/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./ldap-connection-enums";
|
||||||
|
export * from "./ldap-connection-fns";
|
||||||
|
export * from "./ldap-connection-schemas";
|
||||||
|
export * from "./ldap-connection-types";
|
@@ -0,0 +1,7 @@
|
|||||||
|
export enum LdapConnectionMethod {
|
||||||
|
SimpleBind = "simple-bind"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LdapProvider {
|
||||||
|
ActiveDirectory = "active-directory"
|
||||||
|
}
|
102
backend/src/services/app-connection/ldap/ldap-connection-fns.ts
Normal file
102
backend/src/services/app-connection/ldap/ldap-connection-fns.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import ldap from "ldapjs";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
import { LdapConnectionMethod } from "./ldap-connection-enums";
|
||||||
|
import { TLdapConnectionConfig } from "./ldap-connection-types";
|
||||||
|
|
||||||
|
export const getLdapConnectionListItem = () => {
|
||||||
|
return {
|
||||||
|
name: "LDAP" as const,
|
||||||
|
app: AppConnection.LDAP as const,
|
||||||
|
methods: Object.values(LdapConnectionMethod) as [LdapConnectionMethod.SimpleBind]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const LDAP_TIMEOUT = 15_000;
|
||||||
|
|
||||||
|
export const getLdapConnectionClient = async ({
|
||||||
|
url,
|
||||||
|
dn,
|
||||||
|
password,
|
||||||
|
sslCertificate,
|
||||||
|
sslRejectUnauthorized = true
|
||||||
|
}: TLdapConnectionConfig["credentials"]) => {
|
||||||
|
await blockLocalAndPrivateIpAddresses(url);
|
||||||
|
|
||||||
|
const isSSL = url.startsWith("ldaps");
|
||||||
|
|
||||||
|
return new Promise<ldap.Client>((resolve, reject) => {
|
||||||
|
const client = ldap.createClient({
|
||||||
|
url,
|
||||||
|
timeout: LDAP_TIMEOUT,
|
||||||
|
connectTimeout: LDAP_TIMEOUT,
|
||||||
|
tlsOptions: isSSL
|
||||||
|
? {
|
||||||
|
rejectUnauthorized: sslRejectUnauthorized,
|
||||||
|
ca: sslCertificate ? [sslCertificate] : undefined
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("error", (err: Error) => {
|
||||||
|
logger.error(err, "LDAP Error");
|
||||||
|
client.destroy();
|
||||||
|
reject(new Error(`Provider Error - ${err.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("connectError", (err: Error) => {
|
||||||
|
logger.error(err, "LDAP Connection Error");
|
||||||
|
client.destroy();
|
||||||
|
reject(new Error(`Provider Connect Error - ${err.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("connectRefused", (err: Error) => {
|
||||||
|
logger.error(err, "LDAP Connection Refused");
|
||||||
|
client.destroy();
|
||||||
|
reject(new Error(`Provider Connection Refused - ${err.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("connectTimeout", (err: Error) => {
|
||||||
|
logger.error(err, "LDAP Connection Timeout");
|
||||||
|
client.destroy();
|
||||||
|
reject(new Error(`Provider Connection Timeout - ${err.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("connect", () => {
|
||||||
|
client.bind(dn, password, (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error(err, "LDAP Bind Error");
|
||||||
|
reject(new Error(`Bind Error: ${err.message}`));
|
||||||
|
client.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(client);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => {
|
||||||
|
let client: ldap.Client | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
client = await getLdapConnectionClient(credentials);
|
||||||
|
|
||||||
|
// this shouldn't occur as handle connection error events in client but here as fallback
|
||||||
|
if (!client.connected) {
|
||||||
|
throw new BadRequestError({ message: "Unable to connect to LDAP server" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return credentials;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate connection: ${(e as Error).message || "verify credentials"}`
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
client?.destroy();
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,93 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { DistinguishedNameRegex } from "@app/lib/regex";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
BaseAppConnectionSchema,
|
||||||
|
GenericCreateAppConnectionFieldsSchema,
|
||||||
|
GenericUpdateAppConnectionFieldsSchema
|
||||||
|
} from "@app/services/app-connection/app-connection-schemas";
|
||||||
|
|
||||||
|
import { LdapConnectionMethod, LdapProvider } from "./ldap-connection-enums";
|
||||||
|
|
||||||
|
export const LdapConnectionSimpleBindCredentialsSchema = z.object({
|
||||||
|
provider: z.nativeEnum(LdapProvider).describe(AppConnections.CREDENTIALS.LDAP.provider),
|
||||||
|
url: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "URL required")
|
||||||
|
.regex(new RE2(/^ldaps?:\/\//))
|
||||||
|
.describe(AppConnections.CREDENTIALS.LDAP.url),
|
||||||
|
dn: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com")
|
||||||
|
.min(1, "Distinguished Name (DN) required")
|
||||||
|
.describe(AppConnections.CREDENTIALS.LDAP.dn),
|
||||||
|
password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.LDAP.password),
|
||||||
|
sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.LDAP.sslRejectUnauthorized),
|
||||||
|
sslCertificate: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.transform((value) => value || undefined)
|
||||||
|
.optional()
|
||||||
|
.describe(AppConnections.CREDENTIALS.LDAP.sslCertificate)
|
||||||
|
});
|
||||||
|
|
||||||
|
const BaseLdapConnectionSchema = BaseAppConnectionSchema.extend({
|
||||||
|
app: z.literal(AppConnection.LDAP)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LdapConnectionSchema = z.intersection(
|
||||||
|
BaseLdapConnectionSchema,
|
||||||
|
z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(LdapConnectionMethod.SimpleBind),
|
||||||
|
credentials: LdapConnectionSimpleBindCredentialsSchema
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SanitizedLdapConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseLdapConnectionSchema.extend({
|
||||||
|
method: z.literal(LdapConnectionMethod.SimpleBind),
|
||||||
|
credentials: LdapConnectionSimpleBindCredentialsSchema.pick({
|
||||||
|
provider: true,
|
||||||
|
url: true,
|
||||||
|
dn: true,
|
||||||
|
sslRejectUnauthorized: true,
|
||||||
|
sslCertificate: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(LdapConnectionMethod.SimpleBind).describe(AppConnections.CREATE(AppConnection.LDAP).method),
|
||||||
|
credentials: LdapConnectionSimpleBindCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.LDAP).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateLdapConnectionSchema = ValidateLdapConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.LDAP)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateLdapConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: LdapConnectionSimpleBindCredentialsSchema.optional().describe(
|
||||||
|
AppConnections.UPDATE(AppConnection.LDAP).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.LDAP));
|
||||||
|
|
||||||
|
export const LdapConnectionListItemSchema = z.object({
|
||||||
|
name: z.literal("LDAP"),
|
||||||
|
app: z.literal(AppConnection.LDAP),
|
||||||
|
// the below is preferable but currently breaks with our zod to json schema parser
|
||||||
|
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
|
||||||
|
methods: z.nativeEnum(LdapConnectionMethod).array()
|
||||||
|
});
|
@@ -0,0 +1,22 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateLdapConnectionSchema,
|
||||||
|
LdapConnectionSchema,
|
||||||
|
ValidateLdapConnectionCredentialsSchema
|
||||||
|
} from "./ldap-connection-schemas";
|
||||||
|
|
||||||
|
export type TLdapConnection = z.infer<typeof LdapConnectionSchema>;
|
||||||
|
|
||||||
|
export type TLdapConnectionInput = z.infer<typeof CreateLdapConnectionSchema> & {
|
||||||
|
app: AppConnection.LDAP;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TLdapConnectionConfig = DiscriminativePick<TLdapConnection, "method" | "app" | "credentials"> & {
|
||||||
|
orgId: string;
|
||||||
|
};
|
@@ -31,7 +31,8 @@ export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [
|
|||||||
port: true,
|
port: true,
|
||||||
username: true,
|
username: true,
|
||||||
sslEnabled: true,
|
sslEnabled: true,
|
||||||
sslRejectUnauthorized: true
|
sslRejectUnauthorized: true,
|
||||||
|
sslCertificate: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
@@ -29,7 +29,8 @@ export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method",
|
|||||||
port: true,
|
port: true,
|
||||||
username: true,
|
username: true,
|
||||||
sslEnabled: true,
|
sslEnabled: true,
|
||||||
sslRejectUnauthorized: true
|
sslRejectUnauthorized: true,
|
||||||
|
sslCertificate: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
4
backend/src/services/app-connection/teamcity/index.ts
Normal file
4
backend/src/services/app-connection/teamcity/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./teamcity-connection-enums";
|
||||||
|
export * from "./teamcity-connection-fns";
|
||||||
|
export * from "./teamcity-connection-schemas";
|
||||||
|
export * from "./teamcity-connection-types";
|
@@ -0,0 +1,3 @@
|
|||||||
|
export enum TeamCityConnectionMethod {
|
||||||
|
AccessToken = "access-token"
|
||||||
|
}
|
@@ -0,0 +1,74 @@
|
|||||||
|
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 { TeamCityConnectionMethod } from "./teamcity-connection-enums";
|
||||||
|
import {
|
||||||
|
TTeamCityConnection,
|
||||||
|
TTeamCityConnectionConfig,
|
||||||
|
TTeamCityListProjectsResponse
|
||||||
|
} from "./teamcity-connection-types";
|
||||||
|
|
||||||
|
export const getTeamCityInstanceUrl = async (config: TTeamCityConnectionConfig) => {
|
||||||
|
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
|
||||||
|
|
||||||
|
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||||
|
|
||||||
|
return instanceUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTeamCityConnectionListItem = () => {
|
||||||
|
return {
|
||||||
|
name: "TeamCity" as const,
|
||||||
|
app: AppConnection.TeamCity as const,
|
||||||
|
methods: Object.values(TeamCityConnectionMethod) as [TeamCityConnectionMethod.AccessToken]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateTeamCityConnectionCredentials = async (config: TTeamCityConnectionConfig) => {
|
||||||
|
const instanceUrl = await getTeamCityInstanceUrl(config);
|
||||||
|
|
||||||
|
const { accessToken } = config.credentials;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await request.get(`${instanceUrl}/app/rest/server`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} 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"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.credentials;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listTeamCityProjects = async (appConnection: TTeamCityConnection) => {
|
||||||
|
const instanceUrl = await getTeamCityInstanceUrl(appConnection);
|
||||||
|
const { accessToken } = appConnection.credentials;
|
||||||
|
|
||||||
|
const resp = await request.get<TTeamCityListProjectsResponse>(
|
||||||
|
`${instanceUrl}/app/rest/projects?fields=project(id,name,buildTypes(buildType(id,name)))`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter out the root project. Should not be seen by users.
|
||||||
|
return resp.data.project.filter((proj) => proj.id !== "_Root");
|
||||||
|
};
|
@@ -0,0 +1,70 @@
|
|||||||
|
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 { TeamCityConnectionMethod } from "./teamcity-connection-enums";
|
||||||
|
|
||||||
|
export const TeamCityConnectionAccessTokenCredentialsSchema = z.object({
|
||||||
|
accessToken: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Access Token required")
|
||||||
|
.describe(AppConnections.CREDENTIALS.TEAMCITY.accessToken),
|
||||||
|
instanceUrl: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.url("Invalid Instance URL")
|
||||||
|
.min(1, "Instance URL required")
|
||||||
|
.describe(AppConnections.CREDENTIALS.TEAMCITY.instanceUrl)
|
||||||
|
});
|
||||||
|
|
||||||
|
const BaseTeamCityConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.TeamCity) });
|
||||||
|
|
||||||
|
export const TeamCityConnectionSchema = BaseTeamCityConnectionSchema.extend({
|
||||||
|
method: z.literal(TeamCityConnectionMethod.AccessToken),
|
||||||
|
credentials: TeamCityConnectionAccessTokenCredentialsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SanitizedTeamCityConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseTeamCityConnectionSchema.extend({
|
||||||
|
method: z.literal(TeamCityConnectionMethod.AccessToken),
|
||||||
|
credentials: TeamCityConnectionAccessTokenCredentialsSchema.pick({
|
||||||
|
instanceUrl: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ValidateTeamCityConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z
|
||||||
|
.literal(TeamCityConnectionMethod.AccessToken)
|
||||||
|
.describe(AppConnections.CREATE(AppConnection.TeamCity).method),
|
||||||
|
credentials: TeamCityConnectionAccessTokenCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.TeamCity).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateTeamCityConnectionSchema = ValidateTeamCityConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.TeamCity)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateTeamCityConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: TeamCityConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||||
|
AppConnections.UPDATE(AppConnection.TeamCity).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TeamCity));
|
||||||
|
|
||||||
|
export const TeamCityConnectionListItemSchema = z.object({
|
||||||
|
name: z.literal("TeamCity"),
|
||||||
|
app: z.literal(AppConnection.TeamCity),
|
||||||
|
methods: z.nativeEnum(TeamCityConnectionMethod).array()
|
||||||
|
});
|
@@ -0,0 +1,28 @@
|
|||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import { listTeamCityProjects } from "./teamcity-connection-fns";
|
||||||
|
import { TTeamCityConnection } from "./teamcity-connection-types";
|
||||||
|
|
||||||
|
type TGetAppConnectionFunc = (
|
||||||
|
app: AppConnection,
|
||||||
|
connectionId: string,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => Promise<TTeamCityConnection>;
|
||||||
|
|
||||||
|
export const teamcityConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||||
|
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.TeamCity, connectionId, actor);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const projects = await listTeamCityProjects(appConnection);
|
||||||
|
return projects;
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listProjects
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,43 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateTeamCityConnectionSchema,
|
||||||
|
TeamCityConnectionSchema,
|
||||||
|
ValidateTeamCityConnectionCredentialsSchema
|
||||||
|
} from "./teamcity-connection-schemas";
|
||||||
|
|
||||||
|
export type TTeamCityConnection = z.infer<typeof TeamCityConnectionSchema>;
|
||||||
|
|
||||||
|
export type TTeamCityConnectionInput = z.infer<typeof CreateTeamCityConnectionSchema> & {
|
||||||
|
app: AppConnection.TeamCity;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateTeamCityConnectionCredentialsSchema = typeof ValidateTeamCityConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TTeamCityConnectionConfig = DiscriminativePick<
|
||||||
|
TTeamCityConnectionInput,
|
||||||
|
"method" | "app" | "credentials"
|
||||||
|
> & {
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTeamCityProject = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTeamCityProjectWithBuildTypes = TTeamCityProject & {
|
||||||
|
buildTypes: {
|
||||||
|
buildType: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTeamCityListProjectsResponse = {
|
||||||
|
project: TTeamCityProjectWithBuildTypes[];
|
||||||
|
};
|
@@ -47,7 +47,10 @@ export const tokenDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findTokenSessions = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
|
const findTokenSessions = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const sessions = await (tx || db.replicaNode())(TableName.AuthTokenSession).where(filter);
|
const sessions = await (tx || db.replicaNode())(TableName.AuthTokenSession)
|
||||||
|
.where(filter)
|
||||||
|
.orderBy("lastUsed", "desc");
|
||||||
|
|
||||||
return sessions;
|
return sessions;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ name: "Find all token session", error });
|
throw new DatabaseError({ name: "Find all token session", error });
|
||||||
|
@@ -151,6 +151,9 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
|||||||
|
|
||||||
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
|
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
|
||||||
|
|
||||||
|
const revokeMySessionById = async (userId: string, sessionId: string) =>
|
||||||
|
tokenDAL.deleteTokenSession({ userId, id: sessionId });
|
||||||
|
|
||||||
const validateRefreshToken = async (refreshToken?: string) => {
|
const validateRefreshToken = async (refreshToken?: string) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
if (!refreshToken)
|
if (!refreshToken)
|
||||||
@@ -223,6 +226,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
|||||||
clearTokenSessionById,
|
clearTokenSessionById,
|
||||||
getTokenSessionByUser,
|
getTokenSessionByUser,
|
||||||
revokeAllMySessions,
|
revokeAllMySessions,
|
||||||
|
revokeMySessionById,
|
||||||
validateRefreshToken,
|
validateRefreshToken,
|
||||||
fnValidateJwtIdentity,
|
fnValidateJwtIdentity,
|
||||||
getUserTokenSessionById
|
getUserTokenSessionById
|
||||||
|
@@ -3,6 +3,8 @@ import jwt from "jsonwebtoken";
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas";
|
import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||||
|
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
|
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";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
@@ -10,7 +12,9 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
|||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
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";
|
||||||
@@ -28,7 +32,14 @@ import {
|
|||||||
TOauthTokenExchangeDTO,
|
TOauthTokenExchangeDTO,
|
||||||
TVerifyMfaTokenDTO
|
TVerifyMfaTokenDTO
|
||||||
} from "./auth-login-type";
|
} from "./auth-login-type";
|
||||||
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "./auth-type";
|
import {
|
||||||
|
ActorType,
|
||||||
|
AuthMethod,
|
||||||
|
AuthModeJwtTokenPayload,
|
||||||
|
AuthModeMfaJwtTokenPayload,
|
||||||
|
AuthTokenType,
|
||||||
|
MfaMethod
|
||||||
|
} from "./auth-type";
|
||||||
|
|
||||||
type TAuthLoginServiceFactoryDep = {
|
type TAuthLoginServiceFactoryDep = {
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
@@ -36,6 +47,7 @@ type TAuthLoginServiceFactoryDep = {
|
|||||||
tokenService: TAuthTokenServiceFactory;
|
tokenService: TAuthTokenServiceFactory;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
||||||
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||||
@@ -44,7 +56,8 @@ export const authLoginServiceFactory = ({
|
|||||||
tokenService,
|
tokenService,
|
||||||
smtpService,
|
smtpService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
totpService
|
totpService,
|
||||||
|
auditLogService
|
||||||
}: TAuthLoginServiceFactoryDep) => {
|
}: TAuthLoginServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Private
|
* Private
|
||||||
@@ -412,6 +425,55 @@ export const authLoginServiceFactory = ({
|
|||||||
mfaMethod: decodedToken.mfaMethod
|
mfaMethod: decodedToken.mfaMethod
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// In the event of this being a break-glass request (non-saml / non-oidc, when either is enforced)
|
||||||
|
if (
|
||||||
|
selectedOrg.authEnforced &&
|
||||||
|
selectedOrg.bypassOrgAuthEnabled &&
|
||||||
|
!isAuthMethodSaml(decodedToken.authMethod) &&
|
||||||
|
decodedToken.authMethod !== AuthMethod.OIDC
|
||||||
|
) {
|
||||||
|
await auditLogService.createAuditLog({
|
||||||
|
orgId: organizationId,
|
||||||
|
ipAddress,
|
||||||
|
userAgent,
|
||||||
|
userAgentType: getUserAgentType(userAgent),
|
||||||
|
actor: {
|
||||||
|
type: ActorType.USER,
|
||||||
|
metadata: {
|
||||||
|
email: user.email,
|
||||||
|
userId: user.id,
|
||||||
|
username: user.username
|
||||||
|
}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.ORG_ADMIN_BYPASS_SSO,
|
||||||
|
metadata: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify all admins via email (besides the actor)
|
||||||
|
const orgAdmins = await orgDAL.findOrgMembersByRole(organizationId, OrgMembershipRole.Admin);
|
||||||
|
const adminEmails = orgAdmins
|
||||||
|
.filter((admin) => admin.user.id !== user.id)
|
||||||
|
.map((admin) => admin.user.email)
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
|
||||||
|
if (adminEmails.length > 0) {
|
||||||
|
await smtpService.sendMail({
|
||||||
|
recipients: adminEmails,
|
||||||
|
subjectLine: "Security Alert: Admin SSO Bypass",
|
||||||
|
substitutions: {
|
||||||
|
email: user.email,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
ip: ipAddress,
|
||||||
|
userAgent,
|
||||||
|
siteUrl: removeTrailingSlash(cfg.SITE_URL || "https://app.infisical.com")
|
||||||
|
},
|
||||||
|
template: SmtpTemplates.OrgAdminBreakglassAccess
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...tokens,
|
...tokens,
|
||||||
isMfaEnabled: false
|
isMfaEnabled: false
|
||||||
|
@@ -68,18 +68,15 @@ const awsRegionFromHeader = (authorizationHeader: string): string | null => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isValidAwsRegion(region: string | null): boolean {
|
||||||
|
const validRegionPattern = new RE2("^[a-z0-9-]+$");
|
||||||
function isValidAwsRegion(region: (string | null)): boolean {
|
if (typeof region !== "string" || region.length === 0 || region.length > 20) {
|
||||||
const validRegionPattern = new RE2('^[a-z0-9-]+$');
|
|
||||||
if (typeof region !== 'string' || region.length === 0 || region.length > 20) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return validRegionPattern.test(region);
|
return validRegionPattern.test(region);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const identityAwsAuthServiceFactory = ({
|
export const identityAwsAuthServiceFactory = ({
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
identityAwsAuthDAL,
|
identityAwsAuthDAL,
|
||||||
|
@@ -435,12 +435,16 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||||
|
|
||||||
|
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
|
||||||
|
if (!identityKubernetesAuth) {
|
||||||
|
throw new NotFoundError({ message: `Failed to find Kubernetes Auth for identity with ID ${identityId}` });
|
||||||
|
}
|
||||||
|
|
||||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
|
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "The identity does not have Kubernetes Auth attached"
|
message: "The identity does not have Kubernetes Auth attached"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
|
|
||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
|
@@ -50,7 +50,7 @@ const getIntegrationSecretsV2 = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// process secrets in current folder
|
// process secrets in current folder
|
||||||
const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId, projectId: dto.projectId });
|
const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId });
|
||||||
|
|
||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
const secretKey = secret.key;
|
const secretKey = secret.key;
|
||||||
@@ -63,7 +63,6 @@ const getIntegrationSecretsV2 = async (
|
|||||||
// if no imports then return secrets in the current folder
|
// if no imports then return secrets in the current folder
|
||||||
if (!secretImports.length) return content;
|
if (!secretImports.length) return content;
|
||||||
const importedSecrets = await fnSecretsV2FromImports({
|
const importedSecrets = await fnSecretsV2FromImports({
|
||||||
projectId: dto.projectId,
|
|
||||||
decryptor: dto.decryptor,
|
decryptor: dto.decryptor,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
|
@@ -787,13 +787,19 @@ export const kmsServiceFactory = ({
|
|||||||
return projectDataKey;
|
return projectDataKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
error,
|
||||||
|
`getProjectSecretManagerKmsDataKey: Failed to get project data key for [projectId=${projectId}]`
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
await lock?.release();
|
await lock?.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!project.kmsSecretManagerEncryptedDataKey) {
|
if (!project.kmsSecretManagerEncryptedDataKey) {
|
||||||
throw new Error("Missing project data key");
|
throw new BadRequestError({ message: "Missing project data key" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const kmsDecryptor = await decryptWithKmsKey({
|
const kmsDecryptor = await decryptWithKmsKey({
|
||||||
|
@@ -2,6 +2,7 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import {
|
import {
|
||||||
|
OrgMembershipRole,
|
||||||
TableName,
|
TableName,
|
||||||
TOrganizations,
|
TOrganizations,
|
||||||
TOrganizationsInsert,
|
TOrganizationsInsert,
|
||||||
@@ -216,9 +217,8 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => {
|
const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const conn = tx || db;
|
const conn = tx || db.replicaNode();
|
||||||
const members = await conn(TableName.OrgMembership)
|
const members = await conn(TableName.OrgMembership)
|
||||||
// .replicaNode()(TableName.OrgMembership)
|
|
||||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
.leftJoin<TUserEncryptionKeys>(
|
.leftJoin<TUserEncryptionKeys>(
|
||||||
@@ -251,6 +251,43 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findOrgMembersByRole = async (orgId: string, role: OrgMembershipRole, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const conn = tx || db.replicaNode();
|
||||||
|
const members = await conn(TableName.OrgMembership)
|
||||||
|
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||||
|
.where(`${TableName.OrgMembership}.role`, role)
|
||||||
|
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin<TUserEncryptionKeys>(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
`${TableName.UserEncryptionKey}.userId`,
|
||||||
|
`${TableName.Users}.id`
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
conn.ref("id").withSchema(TableName.OrgMembership),
|
||||||
|
conn.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||||
|
conn.ref("orgId").withSchema(TableName.OrgMembership),
|
||||||
|
conn.ref("role").withSchema(TableName.OrgMembership),
|
||||||
|
conn.ref("roleId").withSchema(TableName.OrgMembership),
|
||||||
|
conn.ref("status").withSchema(TableName.OrgMembership),
|
||||||
|
conn.ref("username").withSchema(TableName.Users),
|
||||||
|
conn.ref("email").withSchema(TableName.Users),
|
||||||
|
conn.ref("firstName").withSchema(TableName.Users),
|
||||||
|
conn.ref("lastName").withSchema(TableName.Users),
|
||||||
|
conn.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
conn.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||||
|
)
|
||||||
|
.where({ isGhost: false });
|
||||||
|
|
||||||
|
return members.map(({ username, email, firstName, lastName, userId, publicKey, ...data }) => ({
|
||||||
|
...data,
|
||||||
|
user: { username, email, firstName, lastName, id: userId, publicKey }
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find org members by role" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findOrgGhostUser = async (orgId: string) => {
|
const findOrgGhostUser = async (orgId: string) => {
|
||||||
try {
|
try {
|
||||||
const member = await db
|
const member = await db
|
||||||
@@ -472,6 +509,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
findAllOrgsByUserId,
|
findAllOrgsByUserId,
|
||||||
ghostUserExists,
|
ghostUserExists,
|
||||||
findOrgMembersByUsername,
|
findOrgMembersByUsername,
|
||||||
|
findOrgMembersByRole,
|
||||||
findOrgGhostUser,
|
findOrgGhostUser,
|
||||||
create,
|
create,
|
||||||
updateById,
|
updateById,
|
||||||
|
@@ -13,7 +13,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
|||||||
// special query
|
// special query
|
||||||
const findAllProjectMembers = async (
|
const findAllProjectMembers = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
filter: { usernames?: string[]; username?: string; id?: string } = {}
|
filter: { usernames?: string[]; username?: string; id?: string; roles?: string[] } = {}
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const docs = await db
|
const docs = await db
|
||||||
@@ -31,6 +31,29 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
|||||||
if (filter.id) {
|
if (filter.id) {
|
||||||
void qb.where(`${TableName.ProjectMembership}.id`, filter.id);
|
void qb.where(`${TableName.ProjectMembership}.id`, filter.id);
|
||||||
}
|
}
|
||||||
|
if (filter.roles && filter.roles.length > 0) {
|
||||||
|
void qb.whereExists((subQuery) => {
|
||||||
|
void subQuery
|
||||||
|
.select("role")
|
||||||
|
.from(TableName.ProjectUserMembershipRole)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectRoles,
|
||||||
|
`${TableName.ProjectRoles}.id`,
|
||||||
|
`${TableName.ProjectUserMembershipRole}.customRoleId`
|
||||||
|
)
|
||||||
|
.whereRaw("??.?? = ??.??", [
|
||||||
|
TableName.ProjectUserMembershipRole,
|
||||||
|
"projectMembershipId",
|
||||||
|
TableName.ProjectMembership,
|
||||||
|
"id"
|
||||||
|
])
|
||||||
|
.where((subQb) => {
|
||||||
|
void subQb
|
||||||
|
.whereIn(`${TableName.ProjectUserMembershipRole}.role`, filter.roles as string[])
|
||||||
|
.orWhereIn(`${TableName.ProjectRoles}.slug`, filter.roles as string[]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.join<TUserEncryptionKeys>(
|
.join<TUserEncryptionKeys>(
|
||||||
TableName.UserEncryptionKey,
|
TableName.UserEncryptionKey,
|
||||||
|
@@ -79,7 +79,8 @@ export const projectMembershipServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
includeGroupMembers,
|
includeGroupMembers,
|
||||||
projectId
|
projectId,
|
||||||
|
roles
|
||||||
}: TGetProjectMembershipDTO) => {
|
}: TGetProjectMembershipDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
@@ -91,7 +92,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId, { roles });
|
||||||
|
|
||||||
// projectMembers[0].project
|
// projectMembers[0].project
|
||||||
if (includeGroupMembers) {
|
if (includeGroupMembers) {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TGetProjectMembershipDTO = { includeGroupMembers?: boolean } & TProjectPermission;
|
export type TGetProjectMembershipDTO = { includeGroupMembers?: boolean; roles?: string[] } & TProjectPermission;
|
||||||
export type TLeaveProjectDTO = Omit<TProjectPermission, "actorOrgId" | "actorAuthMethod">;
|
export type TLeaveProjectDTO = Omit<TProjectPermission, "actorOrgId" | "actorAuthMethod">;
|
||||||
export enum ProjectUserMembershipTemporaryMode {
|
export enum ProjectUserMembershipTemporaryMode {
|
||||||
Relative = "relative"
|
Relative = "relative"
|
||||||
|
@@ -73,6 +73,7 @@ import {
|
|||||||
TGetProjectDTO,
|
TGetProjectDTO,
|
||||||
TGetProjectKmsKey,
|
TGetProjectKmsKey,
|
||||||
TGetProjectSlackConfig,
|
TGetProjectSlackConfig,
|
||||||
|
TGetProjectSshConfig,
|
||||||
TListProjectAlertsDTO,
|
TListProjectAlertsDTO,
|
||||||
TListProjectCasDTO,
|
TListProjectCasDTO,
|
||||||
TListProjectCertificateTemplatesDTO,
|
TListProjectCertificateTemplatesDTO,
|
||||||
@@ -92,6 +93,7 @@ import {
|
|||||||
TUpdateProjectKmsDTO,
|
TUpdateProjectKmsDTO,
|
||||||
TUpdateProjectNameDTO,
|
TUpdateProjectNameDTO,
|
||||||
TUpdateProjectSlackConfig,
|
TUpdateProjectSlackConfig,
|
||||||
|
TUpdateProjectSshConfig,
|
||||||
TUpdateProjectVersionLimitDTO,
|
TUpdateProjectVersionLimitDTO,
|
||||||
TUpgradeProjectDTO
|
TUpgradeProjectDTO
|
||||||
} from "./project-types";
|
} from "./project-types";
|
||||||
@@ -104,7 +106,7 @@ export const DEFAULT_PROJECT_ENVS = [
|
|||||||
|
|
||||||
type TProjectServiceFactoryDep = {
|
type TProjectServiceFactoryDep = {
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "create">;
|
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "transaction" | "create" | "findOne" | "updateById">;
|
||||||
projectQueue: TProjectQueueFactory;
|
projectQueue: TProjectQueueFactory;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
@@ -129,7 +131,7 @@ type TProjectServiceFactoryDep = {
|
|||||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||||
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
||||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
||||||
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "find" | "create" | "transaction">;
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "find" | "findOne" | "create" | "transaction">;
|
||||||
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create">;
|
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create">;
|
||||||
sshCertificateDAL: Pick<TSshCertificateDALFactory, "find" | "countSshCertificatesInProject">;
|
sshCertificateDAL: Pick<TSshCertificateDALFactory, "find" | "countSshCertificatesInProject">;
|
||||||
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find">;
|
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find">;
|
||||||
@@ -1327,6 +1329,129 @@ export const projectServiceFactory = ({
|
|||||||
return { secretManagerKmsKey: kmsKey };
|
return { secretManagerKmsKey: kmsKey };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getProjectSshConfig = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId
|
||||||
|
}: TGetProjectSshConfig) => {
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Project with ID '${projectId}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
const projectSshConfig = await projectSshConfigDAL.findOne({
|
||||||
|
projectId: project.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!projectSshConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Project SSH config with ID '${project.id}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectSshConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateProjectSshConfig = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
defaultUserSshCaId,
|
||||||
|
defaultHostSshCaId
|
||||||
|
}: TUpdateProjectSshConfig) => {
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Project with ID '${projectId}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
let projectSshConfig = await projectSshConfigDAL.findOne({
|
||||||
|
projectId: project.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!projectSshConfig) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Project SSH config with ID '${project.id}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
projectSshConfig = await projectSshConfigDAL.transaction(async (tx) => {
|
||||||
|
if (defaultUserSshCaId) {
|
||||||
|
const userSshCa = await sshCertificateAuthorityDAL.findOne(
|
||||||
|
{
|
||||||
|
id: defaultUserSshCaId,
|
||||||
|
projectId: project.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!userSshCa) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "User SSH CA must exist and belong to this project"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultHostSshCaId) {
|
||||||
|
const hostSshCa = await sshCertificateAuthorityDAL.findOne(
|
||||||
|
{
|
||||||
|
id: defaultHostSshCaId,
|
||||||
|
projectId: project.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hostSshCa) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Host SSH CA must exist and belong to this project"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedProjectSshConfig = await projectSshConfigDAL.updateById(
|
||||||
|
projectSshConfig.id,
|
||||||
|
{
|
||||||
|
defaultUserSshCaId,
|
||||||
|
defaultHostSshCaId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return updatedProjectSshConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
return projectSshConfig;
|
||||||
|
};
|
||||||
|
|
||||||
const getProjectSlackConfig = async ({
|
const getProjectSlackConfig = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
@@ -1548,6 +1673,8 @@ export const projectServiceFactory = ({
|
|||||||
getProjectKmsBackup,
|
getProjectKmsBackup,
|
||||||
loadProjectKmsBackup,
|
loadProjectKmsBackup,
|
||||||
getProjectKmsKeys,
|
getProjectKmsKeys,
|
||||||
|
getProjectSshConfig,
|
||||||
|
updateProjectSshConfig,
|
||||||
getProjectSlackConfig,
|
getProjectSlackConfig,
|
||||||
updateProjectSlackConfig,
|
updateProjectSlackConfig,
|
||||||
requestProjectAccess,
|
requestProjectAccess,
|
||||||
|
@@ -159,6 +159,13 @@ export type TListProjectSshCertificatesDTO = {
|
|||||||
limit: number;
|
limit: number;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateProjectSshConfig = {
|
||||||
|
defaultUserSshCaId?: string;
|
||||||
|
defaultHostSshCaId?: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetProjectSshConfig = TProjectPermission;
|
||||||
|
|
||||||
export type TGetProjectSlackConfig = TProjectPermission;
|
export type TGetProjectSlackConfig = TProjectPermission;
|
||||||
|
|
||||||
export type TUpdateProjectSlackConfig = {
|
export type TUpdateProjectSlackConfig = {
|
||||||
|
@@ -159,8 +159,7 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
decryptor,
|
decryptor,
|
||||||
expandSecretReferences,
|
expandSecretReferences,
|
||||||
hasSecretAccess,
|
hasSecretAccess,
|
||||||
viewSecretValue,
|
viewSecretValue
|
||||||
projectId
|
|
||||||
}: {
|
}: {
|
||||||
secretImports: (Omit<TSecretImports, "importEnv"> & {
|
secretImports: (Omit<TSecretImports, "importEnv"> & {
|
||||||
importEnv: { id: string; slug: string; name: string };
|
importEnv: { id: string; slug: string; name: string };
|
||||||
@@ -177,7 +176,6 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
environment: string;
|
environment: string;
|
||||||
}) => Promise<string | undefined>;
|
}) => Promise<string | undefined>;
|
||||||
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
|
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
|
||||||
projectId: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
const cyclicDetector = new Set();
|
const cyclicDetector = new Set();
|
||||||
const stack: {
|
const stack: {
|
||||||
@@ -209,7 +207,10 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
);
|
);
|
||||||
if (!importedFolders.length) continue;
|
if (!importedFolders.length) continue;
|
||||||
|
|
||||||
const importedFolderIds = importedFolders.map((el) => el?.id) as string[];
|
const importedFolderIds = importedFolders.filter(Boolean).map((el) => el?.id) as string[];
|
||||||
|
|
||||||
|
if (!importedFolderIds.length) continue;
|
||||||
|
|
||||||
const importedFolderGroupBySourceImport = groupBy(importedFolders, (i) => `${i?.envId}-${i?.path}`);
|
const importedFolderGroupBySourceImport = groupBy(importedFolders, (i) => `${i?.envId}-${i?.path}`);
|
||||||
|
|
||||||
const importedSecrets = await secretDAL.find(
|
const importedSecrets = await secretDAL.find(
|
||||||
@@ -218,8 +219,7 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
type: SecretType.Shared
|
type: SecretType.Shared
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sort: [["id", "asc"]],
|
sort: [["id", "asc"]]
|
||||||
useCache: { projectId }
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId);
|
const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId);
|
||||||
|
@@ -698,7 +698,6 @@ export const secretImportServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
const importedSecrets = await fnSecretsV2FromImports({
|
const importedSecrets = await fnSecretsV2FromImports({
|
||||||
projectId,
|
|
||||||
secretImports,
|
secretImports,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
viewSecretValue: true,
|
viewSecretValue: true,
|
||||||
|
@@ -10,7 +10,8 @@ export enum SecretSync {
|
|||||||
TerraformCloud = "terraform-cloud",
|
TerraformCloud = "terraform-cloud",
|
||||||
Camunda = "camunda",
|
Camunda = "camunda",
|
||||||
Vercel = "vercel",
|
Vercel = "vercel",
|
||||||
Windmill = "windmill"
|
Windmill = "windmill",
|
||||||
|
TeamCity = "teamcity"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SecretSyncInitialSyncBehavior {
|
export enum SecretSyncInitialSyncBehavior {
|
||||||
|
@@ -27,6 +27,7 @@ import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
|||||||
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||||
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 { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
|
import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
|
||||||
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
|
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
|
||||||
import { WINDMILL_SYNC_LIST_OPTION, WindmillSyncFns } from "./windmill";
|
import { WINDMILL_SYNC_LIST_OPTION, WindmillSyncFns } from "./windmill";
|
||||||
@@ -43,7 +44,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
|||||||
[SecretSync.TerraformCloud]: TERRAFORM_CLOUD_SYNC_LIST_OPTION,
|
[SecretSync.TerraformCloud]: TERRAFORM_CLOUD_SYNC_LIST_OPTION,
|
||||||
[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.TeamCity]: TEAMCITY_SYNC_LIST_OPTION
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listSecretSyncOptions = () => {
|
export const listSecretSyncOptions = () => {
|
||||||
@@ -140,6 +142,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.TeamCity:
|
||||||
|
return TeamCitySyncFns.syncSecrets(secretSync, secretMap);
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||||
@@ -199,6 +203,9 @@ export const SecretSyncFns = {
|
|||||||
case SecretSync.Windmill:
|
case SecretSync.Windmill:
|
||||||
secretMap = await WindmillSyncFns.getSecrets(secretSync);
|
secretMap = await WindmillSyncFns.getSecrets(secretSync);
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.TeamCity:
|
||||||
|
secretMap = await TeamCitySyncFns.getSecrets(secretSync);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||||
@@ -252,6 +259,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.TeamCity:
|
||||||
|
return TeamCitySyncFns.removeSecrets(secretSync, secretMap);
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||||
|
@@ -13,7 +13,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
|||||||
[SecretSync.TerraformCloud]: "Terraform Cloud",
|
[SecretSync.TerraformCloud]: "Terraform Cloud",
|
||||||
[SecretSync.Camunda]: "Camunda",
|
[SecretSync.Camunda]: "Camunda",
|
||||||
[SecretSync.Vercel]: "Vercel",
|
[SecretSync.Vercel]: "Vercel",
|
||||||
[SecretSync.Windmill]: "Windmill"
|
[SecretSync.Windmill]: "Windmill",
|
||||||
|
[SecretSync.TeamCity]: "TeamCity"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||||
@@ -28,5 +29,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
|||||||
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
|
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
|
||||||
[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.TeamCity]: AppConnection.TeamCity
|
||||||
};
|
};
|
||||||
|
@@ -214,7 +214,7 @@ export const secretSyncQueueFactory = ({
|
|||||||
canExpandValue: () => true
|
canExpandValue: () => true
|
||||||
});
|
});
|
||||||
|
|
||||||
const secrets = await secretV2BridgeDAL.findByFolderId({ folderId, projectId });
|
const secrets = await secretV2BridgeDAL.findByFolderId({ folderId });
|
||||||
|
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
secrets.map(async (secret) => {
|
secrets.map(async (secret) => {
|
||||||
@@ -244,7 +244,6 @@ export const secretSyncQueueFactory = ({
|
|||||||
|
|
||||||
if (secretImports.length) {
|
if (secretImports.length) {
|
||||||
const importedSecrets = await fnSecretsV2FromImports({
|
const importedSecrets = await fnSecretsV2FromImports({
|
||||||
projectId,
|
|
||||||
decryptor: decryptSecretValue,
|
decryptor: decryptSecretValue,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
@@ -357,8 +356,11 @@ export const secretSyncQueueFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (Object.hasOwn(secretMap, key)) {
|
if (Object.hasOwn(secretMap, key)) {
|
||||||
|
// Only update secrets if the source value is not empty
|
||||||
|
if (value) {
|
||||||
secretsToUpdate.push(secret);
|
secretsToUpdate.push(secret);
|
||||||
if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination) importedSecretMap[key] = secretData;
|
if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination) importedSecretMap[key] = secretData;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
secretsToCreate.push(secret);
|
secretsToCreate.push(secret);
|
||||||
importedSecretMap[key] = secretData;
|
importedSecretMap[key] = secretData;
|
||||||
|
@@ -61,6 +61,12 @@ import {
|
|||||||
THumanitecSyncListItem,
|
THumanitecSyncListItem,
|
||||||
THumanitecSyncWithCredentials
|
THumanitecSyncWithCredentials
|
||||||
} from "./humanitec";
|
} from "./humanitec";
|
||||||
|
import {
|
||||||
|
TTeamCitySync,
|
||||||
|
TTeamCitySyncInput,
|
||||||
|
TTeamCitySyncListItem,
|
||||||
|
TTeamCitySyncWithCredentials
|
||||||
|
} from "./teamcity/teamcity-sync-types";
|
||||||
import {
|
import {
|
||||||
TTerraformCloudSync,
|
TTerraformCloudSync,
|
||||||
TTerraformCloudSyncInput,
|
TTerraformCloudSyncInput,
|
||||||
@@ -81,7 +87,8 @@ export type TSecretSync =
|
|||||||
| TTerraformCloudSync
|
| TTerraformCloudSync
|
||||||
| TCamundaSync
|
| TCamundaSync
|
||||||
| TVercelSync
|
| TVercelSync
|
||||||
| TWindmillSync;
|
| TWindmillSync
|
||||||
|
| TTeamCitySync;
|
||||||
|
|
||||||
export type TSecretSyncWithCredentials =
|
export type TSecretSyncWithCredentials =
|
||||||
| TAwsParameterStoreSyncWithCredentials
|
| TAwsParameterStoreSyncWithCredentials
|
||||||
@@ -95,7 +102,8 @@ export type TSecretSyncWithCredentials =
|
|||||||
| TTerraformCloudSyncWithCredentials
|
| TTerraformCloudSyncWithCredentials
|
||||||
| TCamundaSyncWithCredentials
|
| TCamundaSyncWithCredentials
|
||||||
| TVercelSyncWithCredentials
|
| TVercelSyncWithCredentials
|
||||||
| TWindmillSyncWithCredentials;
|
| TWindmillSyncWithCredentials
|
||||||
|
| TTeamCitySyncWithCredentials;
|
||||||
|
|
||||||
export type TSecretSyncInput =
|
export type TSecretSyncInput =
|
||||||
| TAwsParameterStoreSyncInput
|
| TAwsParameterStoreSyncInput
|
||||||
@@ -109,7 +117,8 @@ export type TSecretSyncInput =
|
|||||||
| TTerraformCloudSyncInput
|
| TTerraformCloudSyncInput
|
||||||
| TCamundaSyncInput
|
| TCamundaSyncInput
|
||||||
| TVercelSyncInput
|
| TVercelSyncInput
|
||||||
| TWindmillSyncInput;
|
| TWindmillSyncInput
|
||||||
|
| TTeamCitySyncInput;
|
||||||
|
|
||||||
export type TSecretSyncListItem =
|
export type TSecretSyncListItem =
|
||||||
| TAwsParameterStoreSyncListItem
|
| TAwsParameterStoreSyncListItem
|
||||||
@@ -123,7 +132,8 @@ export type TSecretSyncListItem =
|
|||||||
| TTerraformCloudSyncListItem
|
| TTerraformCloudSyncListItem
|
||||||
| TCamundaSyncListItem
|
| TCamundaSyncListItem
|
||||||
| TVercelSyncListItem
|
| TVercelSyncListItem
|
||||||
| TWindmillSyncListItem;
|
| TWindmillSyncListItem
|
||||||
|
| TTeamCitySyncListItem;
|
||||||
|
|
||||||
export type TSyncOptionsConfig = {
|
export type TSyncOptionsConfig = {
|
||||||
canImportSecrets: boolean;
|
canImportSecrets: boolean;
|
||||||
|
4
backend/src/services/secret-sync/teamcity/index.ts
Normal file
4
backend/src/services/secret-sync/teamcity/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./teamcity-sync-constants";
|
||||||
|
export * from "./teamcity-sync-fns";
|
||||||
|
export * from "./teamcity-sync-schemas";
|
||||||
|
export * from "./teamcity-sync-types";
|
@@ -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 TEAMCITY_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||||
|
name: "TeamCity",
|
||||||
|
destination: SecretSync.TeamCity,
|
||||||
|
connection: AppConnection.TeamCity,
|
||||||
|
canImportSecrets: true
|
||||||
|
};
|
183
backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts
Normal file
183
backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { getTeamCityInstanceUrl } from "@app/services/app-connection/teamcity";
|
||||||
|
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||||
|
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
import {
|
||||||
|
TDeleteTeamCityVariable,
|
||||||
|
TPostTeamCityVariable,
|
||||||
|
TTeamCityListVariables,
|
||||||
|
TTeamCityListVariablesResponse,
|
||||||
|
TTeamCitySyncWithCredentials
|
||||||
|
} from "@app/services/secret-sync/teamcity/teamcity-sync-types";
|
||||||
|
|
||||||
|
// Note: Most variables won't be returned with a value due to them being a "password" type (starting with "env.").
|
||||||
|
// TeamCity API returns empty string for password-type variables for security reasons.
|
||||||
|
const listTeamCityVariables = async ({ instanceUrl, accessToken, project, buildConfig }: TTeamCityListVariables) => {
|
||||||
|
const { data } = await request.get<TTeamCityListVariablesResponse>(
|
||||||
|
buildConfig
|
||||||
|
? `${instanceUrl}/app/rest/buildTypes/${encodeURIComponent(buildConfig)}/parameters`
|
||||||
|
: `${instanceUrl}/app/rest/projects/id:${encodeURIComponent(project)}/parameters`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Strips out "env." from map key, but the "name" field still has the original unaltered key.
|
||||||
|
return Object.fromEntries(
|
||||||
|
data.property.map((variable) => [
|
||||||
|
variable.name.startsWith("env.") ? variable.name.substring(4) : variable.name,
|
||||||
|
{ ...variable, value: variable.value || "" } // Password values will be empty strings from the API for security
|
||||||
|
])
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create and update both use the same method
|
||||||
|
const updateTeamCityVariable = async ({
|
||||||
|
instanceUrl,
|
||||||
|
accessToken,
|
||||||
|
project,
|
||||||
|
buildConfig,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
}: TPostTeamCityVariable) => {
|
||||||
|
return request.post(
|
||||||
|
buildConfig
|
||||||
|
? `${instanceUrl}/app/rest/buildTypes/${encodeURIComponent(buildConfig)}/parameters`
|
||||||
|
: `${instanceUrl}/app/rest/projects/id:${encodeURIComponent(project)}/parameters`,
|
||||||
|
{
|
||||||
|
name: key,
|
||||||
|
value,
|
||||||
|
type: {
|
||||||
|
rawValue: "password display='hidden'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTeamCityVariable = async ({
|
||||||
|
instanceUrl,
|
||||||
|
accessToken,
|
||||||
|
project,
|
||||||
|
buildConfig,
|
||||||
|
key
|
||||||
|
}: TDeleteTeamCityVariable) => {
|
||||||
|
return request.delete(
|
||||||
|
buildConfig
|
||||||
|
? `${instanceUrl}/app/rest/buildTypes/${encodeURIComponent(buildConfig)}/parameters/${encodeURIComponent(key)}`
|
||||||
|
: `${instanceUrl}/app/rest/projects/id:${encodeURIComponent(project)}/parameters/${encodeURIComponent(key)}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TeamCitySyncFns = {
|
||||||
|
syncSecrets: async (secretSync: TTeamCitySyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
|
const {
|
||||||
|
connection,
|
||||||
|
destinationConfig: { project, buildConfig }
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
const instanceUrl = await getTeamCityInstanceUrl(connection);
|
||||||
|
const { accessToken } = connection.credentials;
|
||||||
|
|
||||||
|
for await (const entry of Object.entries(secretMap)) {
|
||||||
|
const [key, { value }] = entry;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
instanceUrl,
|
||||||
|
accessToken,
|
||||||
|
project,
|
||||||
|
buildConfig,
|
||||||
|
key: `env.${key}`,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Replace every secret since TeamCity does not return secret values that we can cross-check
|
||||||
|
// No need to differenciate create / update because TeamCity uses the same method for both
|
||||||
|
await updateTeamCityVariable(payload);
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||||
|
|
||||||
|
const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig });
|
||||||
|
|
||||||
|
for await (const [key, variable] of Object.entries(variables)) {
|
||||||
|
if (!(key in secretMap)) {
|
||||||
|
try {
|
||||||
|
await deleteTeamCityVariable({
|
||||||
|
key: variable.name, // We use variable.name instead of key because key is stripped of "env." prefix in listTeamCityVariables().
|
||||||
|
instanceUrl,
|
||||||
|
accessToken,
|
||||||
|
project,
|
||||||
|
buildConfig
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeSecrets: async (secretSync: TTeamCitySyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
|
const {
|
||||||
|
connection,
|
||||||
|
destinationConfig: { project, buildConfig }
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
const instanceUrl = await getTeamCityInstanceUrl(connection);
|
||||||
|
const { accessToken } = connection.credentials;
|
||||||
|
|
||||||
|
const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig });
|
||||||
|
|
||||||
|
for await (const [key, variable] of Object.entries(variables)) {
|
||||||
|
if (key in secretMap) {
|
||||||
|
try {
|
||||||
|
await deleteTeamCityVariable({
|
||||||
|
key: variable.name, // We use variable.name instead of key because key is stripped of "env." prefix in listTeamCityVariables().
|
||||||
|
instanceUrl,
|
||||||
|
accessToken,
|
||||||
|
project,
|
||||||
|
buildConfig
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getSecrets: async (secretSync: TTeamCitySyncWithCredentials) => {
|
||||||
|
const {
|
||||||
|
connection,
|
||||||
|
destinationConfig: { project, buildConfig }
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
const instanceUrl = await getTeamCityInstanceUrl(connection);
|
||||||
|
const { accessToken } = connection.credentials;
|
||||||
|
|
||||||
|
return listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig });
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,44 @@
|
|||||||
|
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 TeamCitySyncDestinationConfigSchema = z.object({
|
||||||
|
project: z.string().trim().min(1, "Project required").describe(SecretSyncs.DESTINATION_CONFIG.TEAMCITY.project),
|
||||||
|
buildConfig: z.string().trim().optional().describe(SecretSyncs.DESTINATION_CONFIG.TEAMCITY.buildConfig)
|
||||||
|
});
|
||||||
|
|
||||||
|
const TeamCitySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||||
|
|
||||||
|
export const TeamCitySyncSchema = BaseSecretSyncSchema(SecretSync.TeamCity, TeamCitySyncOptionsConfig).extend({
|
||||||
|
destination: z.literal(SecretSync.TeamCity),
|
||||||
|
destinationConfig: TeamCitySyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateTeamCitySyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.TeamCity,
|
||||||
|
TeamCitySyncOptionsConfig
|
||||||
|
).extend({
|
||||||
|
destinationConfig: TeamCitySyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateTeamCitySyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.TeamCity,
|
||||||
|
TeamCitySyncOptionsConfig
|
||||||
|
).extend({
|
||||||
|
destinationConfig: TeamCitySyncDestinationConfigSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TeamCitySyncListItemSchema = z.object({
|
||||||
|
name: z.literal("TeamCity"),
|
||||||
|
connection: z.literal(AppConnection.TeamCity),
|
||||||
|
destination: z.literal(SecretSync.TeamCity),
|
||||||
|
canImportSecrets: z.literal(true)
|
||||||
|
});
|
@@ -0,0 +1,46 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TTeamCityConnection } from "@app/services/app-connection/teamcity";
|
||||||
|
|
||||||
|
import { CreateTeamCitySyncSchema, TeamCitySyncListItemSchema, TeamCitySyncSchema } from "./teamcity-sync-schemas";
|
||||||
|
|
||||||
|
export type TTeamCitySync = z.infer<typeof TeamCitySyncSchema>;
|
||||||
|
|
||||||
|
export type TTeamCitySyncInput = z.infer<typeof CreateTeamCitySyncSchema>;
|
||||||
|
|
||||||
|
export type TTeamCitySyncListItem = z.infer<typeof TeamCitySyncListItemSchema>;
|
||||||
|
|
||||||
|
export type TTeamCitySyncWithCredentials = TTeamCitySync & {
|
||||||
|
connection: TTeamCityConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTeamCityVariable = {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
inherited?: boolean;
|
||||||
|
type: {
|
||||||
|
rawValue: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTeamCityListVariablesResponse = {
|
||||||
|
property: (TTeamCityVariable & { value?: string })[];
|
||||||
|
count: number;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTeamCityListVariables = {
|
||||||
|
accessToken: string;
|
||||||
|
instanceUrl: string;
|
||||||
|
project: string;
|
||||||
|
buildConfig?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPostTeamCityVariable = TTeamCityListVariables & {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteTeamCityVariable = TTeamCityListVariables & {
|
||||||
|
key: string;
|
||||||
|
};
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { MongoAbility } from "@casl/ability";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
import { validate as uuidValidate } from "uuid";
|
import { validate as uuidValidate } from "uuid";
|
||||||
|
|
||||||
@@ -15,46 +16,29 @@ import {
|
|||||||
TFindFilter,
|
TFindFilter,
|
||||||
TFindOpt
|
TFindOpt
|
||||||
} from "@app/lib/knex";
|
} from "@app/lib/knex";
|
||||||
import { BufferKeysToString, OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
import type { TFindSecretsByFolderIdsFilter } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
import type {
|
||||||
|
TFindSecretsByFolderIdsFilter,
|
||||||
|
TGetSecretsDTO
|
||||||
|
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
||||||
|
|
||||||
export const SecretDalCacheKeys = {
|
export const SecretServiceCacheKeys = {
|
||||||
get productKey() {
|
get productKey() {
|
||||||
const { INFISICAL_PLATFORM_VERSION } = getConfig();
|
const { INFISICAL_PLATFORM_VERSION } = getConfig();
|
||||||
return `${ProjectType.SecretManager}:${INFISICAL_PLATFORM_VERSION || 0}`;
|
return `${ProjectType.SecretManager}:${INFISICAL_PLATFORM_VERSION || 0}`;
|
||||||
},
|
},
|
||||||
getSecretDalVersion: (projectId: string) => {
|
getSecretDalVersion: (projectId: string) => {
|
||||||
return `${SecretDalCacheKeys.productKey}:${projectId}:${TableName.SecretV2}-dal-version`;
|
return `${SecretServiceCacheKeys.productKey}:${projectId}:${TableName.SecretV2}-dal-version`;
|
||||||
},
|
},
|
||||||
findByFolderIds: (
|
getSecretsOfServiceLayer: (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
version: number,
|
version: number,
|
||||||
{ useCache, tx, ...cacheKey }: Parameters<TSecretV2BridgeDALFactory["findByFolderIds"]>[0]
|
dto: TGetSecretsDTO & { permissionRules: MongoAbility["rules"] }
|
||||||
) => {
|
) => {
|
||||||
return `${SecretDalCacheKeys.productKey}:${projectId}:${
|
return `${SecretServiceCacheKeys.productKey}:${projectId}:${
|
||||||
TableName.SecretV2
|
TableName.SecretV2
|
||||||
}-dal:v${version}:find-by-folder-ids:${generateCacheKeyFromData(cacheKey)}`;
|
}-dal:v${version}:get-secrets-service-layer:${dto.actorId}-${generateCacheKeyFromData(dto)}`;
|
||||||
},
|
|
||||||
findByFolderId: (
|
|
||||||
projectId: string,
|
|
||||||
version: number,
|
|
||||||
{ useCache, tx, ...cacheKey }: Parameters<TSecretV2BridgeDALFactory["findByFolderId"]>[0]
|
|
||||||
) => {
|
|
||||||
return `${SecretDalCacheKeys.productKey}:${projectId}:${
|
|
||||||
TableName.SecretV2
|
|
||||||
}-dal:v${version}:find-by-folder-id:${generateCacheKeyFromData(cacheKey)}`;
|
|
||||||
},
|
|
||||||
find: (projectId: string, version: number, ...args: Parameters<TSecretV2BridgeDALFactory["find"]>) => {
|
|
||||||
const [filter, opts] = args;
|
|
||||||
delete opts?.tx;
|
|
||||||
delete opts?.useCache;
|
|
||||||
return `${SecretDalCacheKeys.productKey}:${projectId}:${
|
|
||||||
TableName.SecretV2
|
|
||||||
}-dal:v${version}:find:${generateCacheKeyFromData({
|
|
||||||
filter,
|
|
||||||
opts
|
|
||||||
})}`;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,14 +48,14 @@ interface TSecretV2DalArg {
|
|||||||
keyStore: TKeyStoreFactory;
|
keyStore: TKeyStoreFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SECRET_DAL_TTL = 5 * 60;
|
export const SECRET_DAL_TTL = 5 * 60;
|
||||||
const SECRET_DAL_VERSION_TTL = 15 * 60;
|
export const SECRET_DAL_VERSION_TTL = 15 * 60;
|
||||||
const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024;
|
export const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024;
|
||||||
export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||||
const secretOrm = ormify(db, TableName.SecretV2);
|
const secretOrm = ormify(db, TableName.SecretV2);
|
||||||
|
|
||||||
const invalidateSecretCacheByProjectId = async (projectId: string) => {
|
const invalidateSecretCacheByProjectId = async (projectId: string) => {
|
||||||
const secretDalVersionKey = SecretDalCacheKeys.getSecretDalVersion(projectId);
|
const secretDalVersionKey = SecretServiceCacheKeys.getSecretDalVersion(projectId);
|
||||||
await keyStore.incrementBy(secretDalVersionKey, 1);
|
await keyStore.incrementBy(secretDalVersionKey, 1);
|
||||||
await keyStore.setExpiry(secretDalVersionKey, SECRET_DAL_VERSION_TTL);
|
await keyStore.setExpiry(secretDalVersionKey, SECRET_DAL_VERSION_TTL);
|
||||||
};
|
};
|
||||||
@@ -128,35 +112,9 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const find = async (
|
const find = async (filter: TFindFilter<TSecretsV2>, opts: TFindOpt<TSecretsV2> = {}) => {
|
||||||
filter: TFindFilter<TSecretsV2>,
|
const { offset, limit, sort, tx } = opts;
|
||||||
opts: TFindOpt<TSecretsV2> & { useCache?: { projectId: string } } = {}
|
|
||||||
) => {
|
|
||||||
const { offset, limit, sort, tx, useCache } = opts;
|
|
||||||
try {
|
try {
|
||||||
let secretDalVersion = 0;
|
|
||||||
if (useCache) {
|
|
||||||
const cachedSecretDalVersion = await keyStore.getItem(
|
|
||||||
SecretDalCacheKeys.getSecretDalVersion(useCache.projectId)
|
|
||||||
);
|
|
||||||
secretDalVersion = Number(cachedSecretDalVersion || 0);
|
|
||||||
const cacheKey = SecretDalCacheKeys.find(useCache.projectId, secretDalVersion, filter, opts);
|
|
||||||
const cachedSecrets = await keyStore.getItem(cacheKey);
|
|
||||||
if (cachedSecrets) {
|
|
||||||
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
|
|
||||||
|
|
||||||
const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[];
|
|
||||||
const sanitizedSecrets = unsanitizedSecrets.map((el) => {
|
|
||||||
const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null;
|
|
||||||
const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null;
|
|
||||||
const createdAt = new Date(el.createdAt);
|
|
||||||
const updatedAt = new Date(el.updatedAt);
|
|
||||||
return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt };
|
|
||||||
});
|
|
||||||
return sanitizedSecrets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = (tx || db)(TableName.SecretV2)
|
const query = (tx || db)(TableName.SecretV2)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
.where(buildFindFilter(filter))
|
.where(buildFindFilter(filter))
|
||||||
@@ -225,22 +183,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (useCache) {
|
|
||||||
const cachedSecrets = data.map((el) => {
|
|
||||||
const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null;
|
|
||||||
const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null;
|
|
||||||
return { ...el, encryptedValue, encryptedComment };
|
|
||||||
});
|
|
||||||
const cache = JSON.stringify(cachedSecrets);
|
|
||||||
if (Buffer.byteLength(cache, "utf8") < MAX_SECRET_CACHE_BYTES) {
|
|
||||||
await keyStore.setItemWithExpiry(
|
|
||||||
SecretDalCacheKeys.find(useCache.projectId, secretDalVersion, filter, opts),
|
|
||||||
SECRET_DAL_TTL,
|
|
||||||
cache
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: `${TableName.SecretV2}: Find` });
|
throw new DatabaseError({ error, name: `${TableName.SecretV2}: Find` });
|
||||||
@@ -345,15 +287,9 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findByFolderId = async (dto: {
|
const findByFolderId = async (dto: { folderId: string; userId?: string; tx?: Knex }) => {
|
||||||
folderId: string;
|
|
||||||
userId?: string;
|
|
||||||
tx?: Knex;
|
|
||||||
projectId: string;
|
|
||||||
useCache?: boolean;
|
|
||||||
}) => {
|
|
||||||
try {
|
try {
|
||||||
const { folderId, tx, projectId } = dto;
|
const { folderId, tx } = dto;
|
||||||
let { userId } = dto;
|
let { userId } = dto;
|
||||||
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo
|
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo
|
||||||
if (userId && !uuidValidate(userId)) {
|
if (userId && !uuidValidate(userId)) {
|
||||||
@@ -361,27 +297,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
userId = undefined;
|
userId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedSecretDalVersion = await keyStore.getItem(SecretDalCacheKeys.getSecretDalVersion(projectId));
|
|
||||||
const secretDalVersion = Number(cachedSecretDalVersion || 0);
|
|
||||||
|
|
||||||
if (dto.useCache) {
|
|
||||||
const cacheKey = SecretDalCacheKeys.findByFolderId(projectId, secretDalVersion, dto);
|
|
||||||
const cachedSecrets = await keyStore.getItem(cacheKey);
|
|
||||||
if (cachedSecrets) {
|
|
||||||
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
|
|
||||||
|
|
||||||
const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[];
|
|
||||||
const sanitizedSecrets = unsanitizedSecrets.map((el) => {
|
|
||||||
const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null;
|
|
||||||
const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null;
|
|
||||||
const createdAt = new Date(el.createdAt);
|
|
||||||
const updatedAt = new Date(el.updatedAt);
|
|
||||||
return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt };
|
|
||||||
});
|
|
||||||
return sanitizedSecrets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const secs = await (tx || db.replicaNode())(TableName.SecretV2)
|
const secs = await (tx || db.replicaNode())(TableName.SecretV2)
|
||||||
.where({ folderId })
|
.where({ folderId })
|
||||||
.where((bd) => {
|
.where((bd) => {
|
||||||
@@ -437,22 +352,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
if (dto.useCache) {
|
|
||||||
const newCachedSecrets = data.map((el) => {
|
|
||||||
const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null;
|
|
||||||
const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null;
|
|
||||||
return { ...el, encryptedValue, encryptedComment };
|
|
||||||
});
|
|
||||||
const cache = JSON.stringify(newCachedSecrets);
|
|
||||||
|
|
||||||
if (Buffer.byteLength(cache, "utf8") < MAX_SECRET_CACHE_BYTES) {
|
|
||||||
await keyStore.setItemWithExpiry(
|
|
||||||
SecretDalCacheKeys.findByFolderId(projectId, secretDalVersion, dto),
|
|
||||||
SECRET_DAL_TTL,
|
|
||||||
cache
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "get all secret" });
|
throw new DatabaseError({ error, name: "get all secret" });
|
||||||
@@ -542,11 +441,9 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
folderIds: string[];
|
folderIds: string[];
|
||||||
userId?: string;
|
userId?: string;
|
||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
projectId: string;
|
|
||||||
filters?: TFindSecretsByFolderIdsFilter;
|
filters?: TFindSecretsByFolderIdsFilter;
|
||||||
useCache?: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { folderIds, tx, filters, useCache, projectId } = dto;
|
const { folderIds, tx, filters } = dto;
|
||||||
let { userId } = dto;
|
let { userId } = dto;
|
||||||
try {
|
try {
|
||||||
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
|
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
|
||||||
@@ -555,26 +452,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
userId = undefined;
|
userId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedSecretDalVersion = await keyStore.getItem(SecretDalCacheKeys.getSecretDalVersion(projectId));
|
|
||||||
const secretDalVersion = Number(cachedSecretDalVersion || 0);
|
|
||||||
if (useCache) {
|
|
||||||
const cacheKey = SecretDalCacheKeys.findByFolderIds(projectId, secretDalVersion, dto);
|
|
||||||
const cachedSecrets = await keyStore.getItem(cacheKey);
|
|
||||||
if (cachedSecrets) {
|
|
||||||
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
|
|
||||||
|
|
||||||
const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[];
|
|
||||||
const sanitizedSecrets = unsanitizedSecrets.map((el) => {
|
|
||||||
const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null;
|
|
||||||
const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null;
|
|
||||||
const createdAt = new Date(el.createdAt);
|
|
||||||
const updatedAt = new Date(el.updatedAt);
|
|
||||||
return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt };
|
|
||||||
});
|
|
||||||
return sanitizedSecrets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = (tx || db.replicaNode())(TableName.SecretV2)
|
const query = (tx || db.replicaNode())(TableName.SecretV2)
|
||||||
.whereIn(`${TableName.SecretV2}.folderId`, folderIds)
|
.whereIn(`${TableName.SecretV2}.folderId`, folderIds)
|
||||||
.where((bd) => {
|
.where((bd) => {
|
||||||
@@ -700,22 +577,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
if (useCache) {
|
|
||||||
const cachedSecrets = data.map((el) => {
|
|
||||||
const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null;
|
|
||||||
const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null;
|
|
||||||
return { ...el, encryptedValue, encryptedComment };
|
|
||||||
});
|
|
||||||
const cache = JSON.stringify(cachedSecrets);
|
|
||||||
|
|
||||||
if (Buffer.byteLength(cache, "utf8") < MAX_SECRET_CACHE_BYTES) {
|
|
||||||
await keyStore.setItemWithExpiry(
|
|
||||||
SecretDalCacheKeys.findByFolderIds(projectId, secretDalVersion, dto),
|
|
||||||
SECRET_DAL_TTL,
|
|
||||||
cache
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@@ -509,7 +509,7 @@ export const expandSecretReferencesFactory = ({
|
|||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!folder) return { value: "", tags: [] };
|
if (!folder) return { value: "", tags: [] };
|
||||||
const secrets = await secretDAL.findByFolderId({ folderId: folder.id, projectId, useCache: true });
|
const secrets = await secretDAL.findByFolderId({ folderId: folder.id });
|
||||||
|
|
||||||
const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => {
|
const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
@@ -25,6 +25,7 @@ import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-app
|
|||||||
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||||
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
||||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||||
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { diff, groupBy } from "@app/lib/fn";
|
import { diff, groupBy } from "@app/lib/fn";
|
||||||
@@ -43,7 +44,12 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
|||||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||||
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
||||||
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
||||||
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
import {
|
||||||
|
MAX_SECRET_CACHE_BYTES,
|
||||||
|
SECRET_DAL_TTL,
|
||||||
|
SecretServiceCacheKeys,
|
||||||
|
TSecretV2BridgeDALFactory
|
||||||
|
} from "./secret-v2-bridge-dal";
|
||||||
import {
|
import {
|
||||||
buildHierarchy,
|
buildHierarchy,
|
||||||
expandSecretReferencesFactory,
|
expandSecretReferencesFactory,
|
||||||
@@ -105,6 +111,7 @@ type TSecretV2BridgeServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "getItem" | "setExpiry" | "setItemWithExpiry" | "deleteItem">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretV2BridgeServiceFactory = ReturnType<typeof secretV2BridgeServiceFactory>;
|
export type TSecretV2BridgeServiceFactory = ReturnType<typeof secretV2BridgeServiceFactory>;
|
||||||
@@ -127,7 +134,8 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL,
|
||||||
secretApprovalRequestSecretDAL,
|
secretApprovalRequestSecretDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
keyStore
|
||||||
}: TSecretV2BridgeServiceFactoryDep) => {
|
}: TSecretV2BridgeServiceFactoryDep) => {
|
||||||
const $validateSecretReferences = async (
|
const $validateSecretReferences = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@@ -800,12 +808,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId);
|
const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId);
|
||||||
|
|
||||||
const secrets = await secretDAL.findByFolderIds({
|
const secrets = await secretDAL.findByFolderIds({
|
||||||
projectId,
|
|
||||||
folderIds: folderMappings.map((folderMapping) => folderMapping.folderId),
|
folderIds: folderMappings.map((folderMapping) => folderMapping.folderId),
|
||||||
userId,
|
userId,
|
||||||
tx: undefined,
|
tx: undefined,
|
||||||
filters,
|
filters
|
||||||
useCache: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
@@ -909,7 +915,8 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
return decryptedSecrets;
|
return decryptedSecrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSecrets = async ({
|
const getSecrets = async (dto: TGetSecretsDTO) => {
|
||||||
|
const {
|
||||||
actorId,
|
actorId,
|
||||||
path,
|
path,
|
||||||
environment,
|
environment,
|
||||||
@@ -923,7 +930,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
expandSecretReferences: shouldExpandSecretReferences,
|
expandSecretReferences: shouldExpandSecretReferences,
|
||||||
throwOnMissingReadValuePermission = true,
|
throwOnMissingReadValuePermission = true,
|
||||||
...params
|
...params
|
||||||
}: TGetSecretsDTO) => {
|
} = dto;
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -934,6 +941,42 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
});
|
});
|
||||||
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
|
||||||
|
|
||||||
|
const cachedSecretDalVersion = await keyStore.getItem(SecretServiceCacheKeys.getSecretDalVersion(projectId));
|
||||||
|
const secretDalVersion = Number(cachedSecretDalVersion || 0);
|
||||||
|
const cacheKey = SecretServiceCacheKeys.getSecretsOfServiceLayer(projectId, secretDalVersion, {
|
||||||
|
...dto,
|
||||||
|
permissionRules: permission.rules
|
||||||
|
});
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor, encryptor: secretManagerEncryptor } =
|
||||||
|
await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const encryptedCachedSecrets = await keyStore.getItem(cacheKey);
|
||||||
|
if (encryptedCachedSecrets) {
|
||||||
|
try {
|
||||||
|
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
|
||||||
|
const cachedSecrets = secretManagerDecryptor({ cipherTextBlob: Buffer.from(encryptedCachedSecrets, "base64") });
|
||||||
|
const { secrets, imports = [] } = JSON.parse(cachedSecrets.toString("utf8")) as {
|
||||||
|
secrets: typeof decryptedSecrets;
|
||||||
|
imports: typeof importedSecrets;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
secrets: secrets.map((el) => ({
|
||||||
|
...el,
|
||||||
|
createdAt: new Date(el.createdAt),
|
||||||
|
updatedAt: new Date(el.updatedAt)
|
||||||
|
})),
|
||||||
|
imports
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, "Secret service layer cache miss");
|
||||||
|
await keyStore.deleteItem(cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let paths: { folderId: string; path: string }[] = [];
|
let paths: { folderId: string; path: string }[] = [];
|
||||||
|
|
||||||
if (recursive) {
|
if (recursive) {
|
||||||
@@ -958,17 +1001,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
const groupedPaths = groupBy(paths, (p) => p.folderId);
|
const groupedPaths = groupBy(paths, (p) => p.folderId);
|
||||||
|
|
||||||
const secrets = await secretDAL.findByFolderIds({
|
const secrets = await secretDAL.findByFolderIds({
|
||||||
projectId,
|
|
||||||
folderIds: paths.map((p) => p.folderId),
|
folderIds: paths.map((p) => p.folderId),
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
tx: undefined,
|
tx: undefined,
|
||||||
filters: params,
|
filters: params
|
||||||
useCache: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
|
||||||
type: KmsDataKey.SecretManager,
|
|
||||||
projectId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// scott: if any of this changes it also needs to be mirrored in secret rotation for getting dashboard secrets
|
// scott: if any of this changes it also needs to be mirrored in secret rotation for getting dashboard secrets
|
||||||
@@ -1086,15 +1122,19 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!includeImports) {
|
if (!includeImports) {
|
||||||
return {
|
const payload = { secrets: decryptedSecrets, imports: [] };
|
||||||
secrets: decryptedSecrets
|
const encryptedUpdatedCachedSecrets = secretManagerEncryptor({
|
||||||
};
|
plainText: Buffer.from(JSON.stringify(payload))
|
||||||
|
}).cipherTextBlob;
|
||||||
|
if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) {
|
||||||
|
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64"));
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId));
|
const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId));
|
||||||
const allowedImports = secretImports.filter(({ isReplication }) => !isReplication);
|
const allowedImports = secretImports.filter(({ isReplication }) => !isReplication);
|
||||||
const importedSecrets = await fnSecretsV2FromImports({
|
const importedSecrets = await fnSecretsV2FromImports({
|
||||||
projectId,
|
|
||||||
viewSecretValue,
|
viewSecretValue,
|
||||||
secretImports: allowedImports,
|
secretImports: allowedImports,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
@@ -1129,10 +1169,14 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
const payload = { secrets: decryptedSecrets, imports: importedSecrets };
|
||||||
secrets: decryptedSecrets,
|
const encryptedUpdatedCachedSecrets = secretManagerEncryptor({
|
||||||
imports: importedSecrets
|
plainText: Buffer.from(JSON.stringify(payload))
|
||||||
};
|
}).cipherTextBlob;
|
||||||
|
if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) {
|
||||||
|
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64"));
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSecretById = async ({ actorId, actor, actorOrgId, actorAuthMethod, secretId }: TGetASecretByIdDTO) => {
|
const getSecretById = async ({ actorId, actor, actorOrgId, actorAuthMethod, secretId }: TGetASecretByIdDTO) => {
|
||||||
@@ -1312,7 +1356,6 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
if (!secret && includeImports) {
|
if (!secret && includeImports) {
|
||||||
const secretImports = await secretImportDAL.find({ folderId, isReplication: false });
|
const secretImports = await secretImportDAL.find({ folderId, isReplication: false });
|
||||||
const importedSecrets = await fnSecretsV2FromImports({
|
const importedSecrets = await fnSecretsV2FromImports({
|
||||||
projectId,
|
|
||||||
secretImports,
|
secretImports,
|
||||||
viewSecretValue,
|
viewSecretValue,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
@@ -2729,7 +2772,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
generatePaths(folderMap).map(({ folderId, path }) => [folderId, path === "/" ? path : path.substring(1)])
|
generatePaths(folderMap).map(({ folderId, path }) => [folderId, path === "/" ? path : path.substring(1)])
|
||||||
);
|
);
|
||||||
|
|
||||||
const secrets = await secretDAL.findByFolderIds({ folderIds: folders.map((f) => f.id), projectId, useCache: true });
|
const secrets = await secretDAL.findByFolderIds({ folderIds: folders.map((f) => f.id) });
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
|
@@ -367,7 +367,7 @@ export const secretQueueFactory = ({
|
|||||||
canExpandValue: () => true
|
canExpandValue: () => true
|
||||||
});
|
});
|
||||||
// process secrets in current folder
|
// process secrets in current folder
|
||||||
const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId, projectId: dto.projectId });
|
const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId });
|
||||||
|
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
secrets.map(async (secret) => {
|
secrets.map(async (secret) => {
|
||||||
@@ -397,7 +397,6 @@ export const secretQueueFactory = ({
|
|||||||
// if no imports then return secrets in the current folder
|
// if no imports then return secrets in the current folder
|
||||||
if (!secretImports.length) return content;
|
if (!secretImports.length) return content;
|
||||||
const importedSecrets = await fnSecretsV2FromImports({
|
const importedSecrets = await fnSecretsV2FromImports({
|
||||||
projectId: dto.projectId,
|
|
||||||
decryptor: dto.decryptor,
|
decryptor: dto.decryptor,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
|
@@ -44,6 +44,7 @@ export enum SmtpTemplates {
|
|||||||
SecretRotationFailed = "secretRotationFailed.handlebars",
|
SecretRotationFailed = "secretRotationFailed.handlebars",
|
||||||
ProjectAccessRequest = "projectAccess.handlebars",
|
ProjectAccessRequest = "projectAccess.handlebars",
|
||||||
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars",
|
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars",
|
||||||
|
OrgAdminBreakglassAccess = "orgAdminBreakglassAccess.handlebars",
|
||||||
ServiceTokenExpired = "serviceTokenExpired.handlebars"
|
ServiceTokenExpired = "serviceTokenExpired.handlebars"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,20 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<title>Organization admin has bypassed SSO</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Infisical</h2>
|
||||||
|
<p>The organization admin {{email}} has bypassed enforced SSO login.</p>
|
||||||
|
<p><strong>Timestamp</strong>: {{timestamp}}</p>
|
||||||
|
<p><strong>IP address</strong>: {{ip}}</p>
|
||||||
|
<p><strong>User agent</strong>: {{userAgent}}</p>
|
||||||
|
<p>If you'd like to disable Admin SSO Bypass, please visit <a href="{{siteUrl}}/organization/settings">Organization Settings</a> > Security.</p>
|
||||||
|
|
||||||
|
{{emailFooter}}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@@ -177,7 +177,6 @@ func issueCredentials(cmd *cobra.Command, args []string) {
|
|||||||
infisicalToken = token.Token
|
infisicalToken = token.Token
|
||||||
} else {
|
} else {
|
||||||
util.RequireLogin()
|
util.RequireLogin()
|
||||||
util.RequireLocalWorkspaceFile()
|
|
||||||
|
|
||||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -411,7 +410,6 @@ func signKey(cmd *cobra.Command, args []string) {
|
|||||||
infisicalToken = token.Token
|
infisicalToken = token.Token
|
||||||
} else {
|
} else {
|
||||||
util.RequireLogin()
|
util.RequireLogin()
|
||||||
util.RequireLocalWorkspaceFile()
|
|
||||||
|
|
||||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -610,8 +608,17 @@ func signKey(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sshConnect(cmd *cobra.Command, args []string) {
|
func sshConnect(cmd *cobra.Command, args []string) {
|
||||||
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
var infisicalToken string
|
||||||
|
|
||||||
|
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||||
|
infisicalToken = token.Token
|
||||||
|
} else {
|
||||||
util.RequireLogin()
|
util.RequireLogin()
|
||||||
util.RequireLocalWorkspaceFile()
|
|
||||||
|
|
||||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -621,14 +628,62 @@ func sshConnect(cmd *cobra.Command, args []string) {
|
|||||||
if loggedInUserDetails.LoginExpired {
|
if loggedInUserDetails.LoginExpired {
|
||||||
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||||
}
|
}
|
||||||
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
infisicalToken := loggedInUserDetails.UserCredentials.JTWToken
|
}
|
||||||
|
|
||||||
writeHostCaToFile, err := cmd.Flags().GetBool("writeHostCaToFile")
|
writeHostCaToFile, err := cmd.Flags().GetBool("writeHostCaToFile")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse --writeHostCaToFile flag")
|
util.HandleError(err, "Unable to parse --writeHostCaToFile flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
outFilePath, err := cmd.Flags().GetString("outFilePath")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname, _ := cmd.Flags().GetString("hostname")
|
||||||
|
loginUser, _ := cmd.Flags().GetString("loginUser")
|
||||||
|
|
||||||
|
var outputDir, privateKeyPath, publicKeyPath, signedKeyPath string
|
||||||
|
if outFilePath != "" {
|
||||||
|
if strings.HasPrefix(outFilePath, "~") {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to resolve home directory")
|
||||||
|
}
|
||||||
|
outFilePath = strings.Replace(outFilePath, "~", homeDir, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(outFilePath, "-cert.pub") {
|
||||||
|
signedKeyPath = outFilePath
|
||||||
|
baseName := strings.TrimSuffix(filepath.Base(outFilePath), "-cert.pub")
|
||||||
|
outputDir = filepath.Dir(outFilePath)
|
||||||
|
privateKeyPath = filepath.Join(outputDir, baseName)
|
||||||
|
publicKeyPath = filepath.Join(outputDir, baseName+".pub")
|
||||||
|
} else {
|
||||||
|
outputDir = outFilePath
|
||||||
|
info, err := os.Stat(outputDir)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll(outputDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to create output directory")
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
util.HandleError(err, "Failed to access output directory")
|
||||||
|
} else if !info.IsDir() {
|
||||||
|
util.PrintErrorMessageAndExit("The provided --outFilePath is not a directory")
|
||||||
|
}
|
||||||
|
fileName := "id_ed25519"
|
||||||
|
privateKeyPath = filepath.Join(outputDir, fileName)
|
||||||
|
publicKeyPath = filepath.Join(outputDir, fileName+".pub")
|
||||||
|
signedKeyPath = filepath.Join(outputDir, fileName+"-cert.pub")
|
||||||
|
}
|
||||||
|
|
||||||
|
if privateKeyPath == "" || publicKeyPath == "" || signedKeyPath == "" {
|
||||||
|
util.PrintErrorMessageAndExit("Failed to resolve file paths for writing credentials")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to get custom headers")
|
util.HandleError(err, "Unable to get custom headers")
|
||||||
@@ -651,12 +706,24 @@ func sshConnect(cmd *cobra.Command, args []string) {
|
|||||||
util.PrintErrorMessageAndExit("You do not have access to any SSH hosts")
|
util.PrintErrorMessageAndExit("You do not have access to any SSH hosts")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt to select host
|
var selectedHost = hosts[0]
|
||||||
|
if hostname != "" {
|
||||||
|
foundHost := false
|
||||||
|
for _, h := range hosts {
|
||||||
|
if h.Hostname == hostname {
|
||||||
|
selectedHost = h
|
||||||
|
foundHost = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundHost {
|
||||||
|
util.PrintErrorMessageAndExit("Specified --hostname not found or not accessible")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
hostNames := make([]string, len(hosts))
|
hostNames := make([]string, len(hosts))
|
||||||
for i, h := range hosts {
|
for i, h := range hosts {
|
||||||
hostNames[i] = h.Hostname
|
hostNames[i] = h.Hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
hostPrompt := promptui.Select{
|
hostPrompt := promptui.Select{
|
||||||
Label: "Select an SSH Host",
|
Label: "Select an SSH Host",
|
||||||
Items: hostNames,
|
Items: hostNames,
|
||||||
@@ -666,18 +733,30 @@ func sshConnect(cmd *cobra.Command, args []string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Prompt failed")
|
util.HandleError(err, "Prompt failed")
|
||||||
}
|
}
|
||||||
selectedHost := hosts[hostIdx]
|
selectedHost = hosts[hostIdx]
|
||||||
|
}
|
||||||
|
|
||||||
// Prompt to select login user
|
var selectedLoginUser string
|
||||||
|
if loginUser != "" {
|
||||||
|
foundLoginUser := false
|
||||||
|
for _, m := range selectedHost.LoginMappings {
|
||||||
|
if m.LoginUser == loginUser {
|
||||||
|
selectedLoginUser = loginUser
|
||||||
|
foundLoginUser = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundLoginUser {
|
||||||
|
util.PrintErrorMessageAndExit("Specified --loginUser not valid for selected host")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if len(selectedHost.LoginMappings) == 0 {
|
if len(selectedHost.LoginMappings) == 0 {
|
||||||
util.PrintErrorMessageAndExit("No login users available for selected host")
|
util.PrintErrorMessageAndExit("No login users available for selected host")
|
||||||
}
|
}
|
||||||
|
|
||||||
loginUsers := make([]string, len(selectedHost.LoginMappings))
|
loginUsers := make([]string, len(selectedHost.LoginMappings))
|
||||||
for i, m := range selectedHost.LoginMappings {
|
for i, m := range selectedHost.LoginMappings {
|
||||||
loginUsers[i] = m.LoginUser
|
loginUsers[i] = m.LoginUser
|
||||||
}
|
}
|
||||||
|
|
||||||
loginPrompt := promptui.Select{
|
loginPrompt := promptui.Select{
|
||||||
Label: "Select Login User",
|
Label: "Select Login User",
|
||||||
Items: loginUsers,
|
Items: loginUsers,
|
||||||
@@ -687,7 +766,8 @@ func sshConnect(cmd *cobra.Command, args []string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Prompt failed")
|
util.HandleError(err, "Prompt failed")
|
||||||
}
|
}
|
||||||
selectedLoginUser := selectedHost.LoginMappings[loginIdx].LoginUser
|
selectedLoginUser = selectedHost.LoginMappings[loginIdx].LoginUser
|
||||||
|
}
|
||||||
|
|
||||||
// Issue SSH creds for host
|
// Issue SSH creds for host
|
||||||
creds, err := infisicalClient.Ssh().IssueSshHostUserCert(selectedHost.ID, infisicalSdk.IssueSshHostUserCertOptions{
|
creds, err := infisicalClient.Ssh().IssueSshHostUserCert(selectedHost.ID, infisicalSdk.IssueSshHostUserCertOptions{
|
||||||
@@ -731,10 +811,27 @@ func sshConnect(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Failed to write Host CA to known_hosts")
|
util.HandleError(err, "Failed to write Host CA to known_hosts")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("📁 Wrote Host CA entry to %s\n", knownHostsPath)
|
fmt.Printf("Successfully wrote Host CA entry to %s\n", knownHostsPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if outFilePath != "" {
|
||||||
|
err = writeToFile(privateKeyPath, creds.PrivateKey, 0600)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to write private key")
|
||||||
|
}
|
||||||
|
err = writeToFile(publicKeyPath, creds.PublicKey, 0644)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to write public key")
|
||||||
|
}
|
||||||
|
err = writeToFile(signedKeyPath, creds.SignedKey, 0644)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to write signed cert")
|
||||||
|
}
|
||||||
|
fmt.Printf("Successfully wrote credentials to %s, %s, and %s\n", privateKeyPath, publicKeyPath, signedKeyPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Load credentials into SSH agent
|
// Load credentials into SSH agent
|
||||||
err = addCredentialsToAgent(creds.PrivateKey, creds.SignedKey)
|
err = addCredentialsToAgent(creds.PrivateKey, creds.SignedKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -769,7 +866,6 @@ func sshAddHost(cmd *cobra.Command, args []string) {
|
|||||||
infisicalToken = token.Token
|
infisicalToken = token.Token
|
||||||
} else {
|
} else {
|
||||||
util.RequireLogin()
|
util.RequireLogin()
|
||||||
util.RequireLocalWorkspaceFile()
|
|
||||||
|
|
||||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1006,16 +1102,20 @@ func init() {
|
|||||||
sshIssueCredentialsCmd.Flags().Bool("addToAgent", false, "Whether to add issued SSH credentials to the SSH agent")
|
sshIssueCredentialsCmd.Flags().Bool("addToAgent", false, "Whether to add issued SSH credentials to the SSH agent")
|
||||||
sshCmd.AddCommand(sshIssueCredentialsCmd)
|
sshCmd.AddCommand(sshIssueCredentialsCmd)
|
||||||
|
|
||||||
sshConnectCmd.Flags().Bool("writeHostCaToFile", true, "Write Host CA public key to ~/.ssh/known_hosts as a separate entry if doesn't already exist")
|
sshConnectCmd.Flags().String("token", "", "Use a machine identity access token")
|
||||||
|
sshConnectCmd.Flags().Bool("write-host-ca-to-file", true, "Write Host CA public key to ~/.ssh/known_hosts as a separate entry if doesn't already exist")
|
||||||
|
sshConnectCmd.Flags().String("hostname", "", "Hostname of the SSH host to connect to")
|
||||||
|
sshConnectCmd.Flags().String("login-user", "", "Login user for the SSH connection")
|
||||||
|
sshConnectCmd.Flags().String("out-file-path", "", "The path to write the SSH credentials to such as ~/.ssh, ./some_folder, ./some_folder/id_rsa-cert.pub. If not provided, the credentials will be added to the SSH agent and used to establish an interactive SSH connection")
|
||||||
sshCmd.AddCommand(sshConnectCmd)
|
sshCmd.AddCommand(sshConnectCmd)
|
||||||
|
|
||||||
sshAddHostCmd.Flags().String("token", "", "Use a machine identity access token")
|
sshAddHostCmd.Flags().String("token", "", "Use a machine identity access token")
|
||||||
sshAddHostCmd.Flags().String("projectId", "", "Project ID the host belongs to (required)")
|
sshAddHostCmd.Flags().String("projectId", "", "Project ID the host belongs to (required)")
|
||||||
sshAddHostCmd.Flags().String("hostname", "", "Hostname of the SSH host (required)")
|
sshAddHostCmd.Flags().String("hostname", "", "Hostname of the SSH host (required)")
|
||||||
sshAddHostCmd.Flags().Bool("writeUserCaToFile", false, "Write User CA public key to /etc/ssh/infisical_user_ca.pub")
|
sshAddHostCmd.Flags().Bool("write-user-ca-to-file", false, "Write User CA public key to /etc/ssh/infisical_user_ca.pub")
|
||||||
sshAddHostCmd.Flags().String("userCaOutFilePath", "/etc/ssh/infisical_user_ca.pub", "Custom file path to write the User CA public key")
|
sshAddHostCmd.Flags().String("user-ca-out-file-path", "/etc/ssh/infisical_user_ca.pub", "Custom file path to write the User CA public key")
|
||||||
sshAddHostCmd.Flags().Bool("writeHostCertToFile", false, "Write SSH host certificate to /etc/ssh/ssh_host_<type>_key-cert.pub")
|
sshAddHostCmd.Flags().Bool("write-host-cert-to-file", false, "Write SSH host certificate to /etc/ssh/ssh_host_<type>_key-cert.pub")
|
||||||
sshAddHostCmd.Flags().Bool("configureSshd", false, "Update TrustedUserCAKeys, HostKey, and HostCertificate in the sshd_config file")
|
sshAddHostCmd.Flags().Bool("configure-sshd", false, "Update TrustedUserCAKeys, HostKey, and HostCertificate in the sshd_config file")
|
||||||
sshAddHostCmd.Flags().Bool("force", false, "Force overwrite of existing certificate files as part of writeUserCaToFile and writeHostCertToFile")
|
sshAddHostCmd.Flags().Bool("force", false, "Force overwrite of existing certificate files as part of writeUserCaToFile and writeHostCertToFile")
|
||||||
|
|
||||||
sshCmd.AddCommand(sshAddHostCmd)
|
sshCmd.AddCommand(sshAddHostCmd)
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/Infisical/infisical-merge/packages/config"
|
"github.com/Infisical/infisical-merge/packages/config"
|
||||||
"github.com/Infisical/infisical-merge/packages/models"
|
"github.com/Infisical/infisical-merge/packages/models"
|
||||||
@@ -85,6 +88,57 @@ var switchCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userGetCmd = &cobra.Command{
|
||||||
|
Use: "get",
|
||||||
|
Short: "Used to get properties of an Infisical profile",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Example: "infisical user get",
|
||||||
|
Args: cobra.ExactArgs(0),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmd.Help()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var userGetTokenCmd = &cobra.Command{
|
||||||
|
Use: "token",
|
||||||
|
Short: "Used to get the access token of an Infisical user",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Example: "infisical user get token",
|
||||||
|
Args: cobra.ExactArgs(0),
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
util.RequireLogin()
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
|
if loggedInUserDetails.LoginExpired {
|
||||||
|
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "[infisical user get token]: Unable to get logged in user token")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenParts := strings.Split(loggedInUserDetails.UserCredentials.JTWToken, ".")
|
||||||
|
if len(tokenParts) != 3 {
|
||||||
|
util.HandleError(errors.New("invalid token format"), "[infisical user get token]: Invalid token format")
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := base64.RawURLEncoding.DecodeString(tokenParts[1])
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "[infisical user get token]: Unable to decode token payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenPayload struct {
|
||||||
|
TokenVersionId string `json:"tokenVersionId"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(payload, &tokenPayload); err != nil {
|
||||||
|
util.HandleError(err, "[infisical user get token]: Unable to parse token payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Session ID:", tokenPayload.TokenVersionId)
|
||||||
|
fmt.Println("Token:", loggedInUserDetails.UserCredentials.JTWToken)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var updateCmd = &cobra.Command{
|
var updateCmd = &cobra.Command{
|
||||||
Use: "update",
|
Use: "update",
|
||||||
Short: "Used to update properties of an Infisical profile",
|
Short: "Used to update properties of an Infisical profile",
|
||||||
@@ -185,6 +239,8 @@ var domainCmd = &cobra.Command{
|
|||||||
func init() {
|
func init() {
|
||||||
updateCmd.AddCommand(domainCmd)
|
updateCmd.AddCommand(domainCmd)
|
||||||
userCmd.AddCommand(updateCmd)
|
userCmd.AddCommand(updateCmd)
|
||||||
|
userGetCmd.AddCommand(userGetTokenCmd)
|
||||||
|
userCmd.AddCommand(userGetCmd)
|
||||||
userCmd.AddCommand(switchCmd)
|
userCmd.AddCommand(switchCmd)
|
||||||
rootCmd.AddCommand(userCmd)
|
rootCmd.AddCommand(userCmd)
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Available"
|
||||||
|
openapi: "GET /api/v1/app-connections/ldap/available"
|
||||||
|
---
|
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v1/app-connections/ldap"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Check out the configuration docs for [LDAP Connections](/integrations/app-connections/ldap) to learn how to obtain
|
||||||
|
the required credentials.
|
||||||
|
</Note>
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/app-connections/ldap/{connectionId}"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v1/app-connections/ldap/{connectionId}"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Name"
|
||||||
|
openapi: "GET /api/v1/app-connections/ldap/connection-name/{connectionName}"
|
||||||
|
---
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user