deconflict merge

This commit is contained in:
Scott Wilson
2025-04-25 17:20:30 -07:00
264 changed files with 4955 additions and 968 deletions
@@ -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,7 @@
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 { 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";
@@ -14,5 +15,6 @@ 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.LdapPassword]: registerLdapPasswordRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter
}; };
@@ -2,6 +2,7 @@ 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 { 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";
@@ -15,7 +16,8 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
PostgresCredentialsRotationListItemSchema, PostgresCredentialsRotationListItemSchema,
MsSqlCredentialsRotationListItemSchema, MsSqlCredentialsRotationListItemSchema,
Auth0ClientSecretRotationListItemSchema, Auth0ClientSecretRotationListItemSchema,
LdapPasswordRotationListItemSchema 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";
@@ -2,7 +2,8 @@ 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" LdapPassword = "ldap-password",
AwsIamUserSecret = "aws-iam-user-secret"
} }
export enum SecretRotationStatus { export enum SecretRotationStatus {
@@ -4,6 +4,7 @@ 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 { 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";
@@ -20,7 +21,8 @@ const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2List
[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.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION
}; };
export const listSecretRotationOptions = () => { export const listSecretRotationOptions = () => {
@@ -5,12 +5,14 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials", [SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials", [SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials",
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret", [SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
[SecretRotation.LdapPassword]: "LDAP Password" [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.LdapPassword]: AppConnection.LDAP,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
}; };
@@ -78,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 = {
@@ -116,7 +117,8 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
[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.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation
}; };
export const secretRotationV2ServiceFactory = ({ export const secretRotationV2ServiceFactory = ({
@@ -12,6 +12,13 @@ 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 { import {
TLdapPasswordRotation, TLdapPasswordRotation,
TLdapPasswordRotationGeneratedCredentials, TLdapPasswordRotationGeneratedCredentials,
@@ -38,30 +45,35 @@ export type TSecretRotationV2 =
| TPostgresCredentialsRotation | TPostgresCredentialsRotation
| TMsSqlCredentialsRotation | TMsSqlCredentialsRotation
| TAuth0ClientSecretRotation | TAuth0ClientSecretRotation
| TLdapPasswordRotation; | TLdapPasswordRotation
| TAwsIamUserSecretRotation;
export type TSecretRotationV2WithConnection = export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection | TPostgresCredentialsRotationWithConnection
| TMsSqlCredentialsRotationWithConnection | TMsSqlCredentialsRotationWithConnection
| TAuth0ClientSecretRotationWithConnection | TAuth0ClientSecretRotationWithConnection
| TLdapPasswordRotationWithConnection; | TLdapPasswordRotationWithConnection
| TAwsIamUserSecretRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials = export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials | TSqlCredentialsRotationGeneratedCredentials
| TAuth0ClientSecretRotationGeneratedCredentials | TAuth0ClientSecretRotationGeneratedCredentials
| TLdapPasswordRotationGeneratedCredentials; | TLdapPasswordRotationGeneratedCredentials
| TAwsIamUserSecretRotationGeneratedCredentials;
export type TSecretRotationV2Input = export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput | TPostgresCredentialsRotationInput
| TMsSqlCredentialsRotationInput | TMsSqlCredentialsRotationInput
| TAuth0ClientSecretRotationInput | TAuth0ClientSecretRotationInput
| TLdapPasswordRotationInput; | TLdapPasswordRotationInput
| TAwsIamUserSecretRotationInput;
export type TSecretRotationV2ListItem = export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem | TPostgresCredentialsRotationListItem
| TMsSqlCredentialsRotationListItem | TMsSqlCredentialsRotationListItem
| TAuth0ClientSecretRotationListItem | TAuth0ClientSecretRotationListItem
| TLdapPasswordRotationListItem; | TLdapPasswordRotationListItem
| TAwsIamUserSecretRotationListItem;
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>; export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
@@ -5,9 +5,12 @@ import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/
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 LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema
]); ]);
+16
View File
@@ -1867,6 +1867,10 @@ export const AppConnections = {
"Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.", "Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.",
sslCertificate: sslCertificate:
"The SSL certificate (PEM format) to use for secure connection when using ldaps:// with a self-signed certificate." "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."
} }
} }
}; };
@@ -2006,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."
} }
} }
}; };
@@ -2086,6 +2094,10 @@ export const SecretRotations = {
}, },
allowedSymbols: 'The allowed symbols to use in the generated password (defaults to "-_.~!*").' 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: {
@@ -2100,6 +2112,10 @@ export const SecretRotations = {
LDAP_PASSWORD: { LDAP_PASSWORD: {
dn: "The name of the secret that the Distinguished Name (DN) of the principal will be mapped to.", 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." 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."
} }
} }
}; };
+10 -2
View File
@@ -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({
@@ -34,6 +34,10 @@ 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
@@ -61,7 +65,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedCamundaConnectionSchema.options, ...SanitizedCamundaConnectionSchema.options,
...SanitizedWindmillConnectionSchema.options, ...SanitizedWindmillConnectionSchema.options,
...SanitizedAuth0ConnectionSchema.options, ...SanitizedAuth0ConnectionSchema.options,
...SanitizedLdapConnectionSchema.options ...SanitizedLdapConnectionSchema.options,
...SanitizedTeamCityConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -79,7 +84,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
CamundaConnectionListItemSchema, CamundaConnectionListItemSchema,
WindmillConnectionListItemSchema, WindmillConnectionListItemSchema,
Auth0ConnectionListItemSchema, Auth0ConnectionListItemSchema,
LdapConnectionListItemSchema 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 };
}
});
}; };
@@ -12,6 +12,7 @@ import { registerHumanitecConnectionRouter } from "./humanitec-connection-router
import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
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";
@@ -34,5 +35,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Camunda]: registerCamundaConnectionRouter, [AppConnection.Camunda]: registerCamundaConnectionRouter,
[AppConnection.Windmill]: registerWindmillConnectionRouter, [AppConnection.Windmill]: registerWindmillConnectionRouter,
[AppConnection.Auth0]: registerAuth0ConnectionRouter, [AppConnection.Auth0]: registerAuth0ConnectionRouter,
[AppConnection.LDAP]: registerLdapConnectionRouter [AppConnection.LDAP]: registerLdapConnectionRouter,
[AppConnection.TeamCity]: registerTeamCityConnectionRouter
}; };
@@ -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;
}
});
};
+117 -2
View File
@@ -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",
@@ -13,7 +13,8 @@ export enum AppConnection {
Camunda = "camunda", Camunda = "camunda",
Windmill = "windmill", Windmill = "windmill",
Auth0 = "auth0", Auth0 = "auth0",
LDAP = "ldap" LDAP = "ldap",
TeamCity = "teamcity"
} }
export enum AWSRegion { export enum AWSRegion {
@@ -44,6 +44,11 @@ import {
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap"; 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,
@@ -73,7 +78,8 @@ export const listAppConnectionOptions = () => {
getCamundaConnectionListItem(), getCamundaConnectionListItem(),
getWindmillConnectionListItem(), getWindmillConnectionListItem(),
getAuth0ConnectionListItem(), getAuth0ConnectionListItem(),
getLdapConnectionListItem() getLdapConnectionListItem(),
getTeamCityConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@@ -138,7 +144,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.LDAP]: validateLdapConnectionCredentials 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);
@@ -170,6 +177,7 @@ 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";
@@ -220,5 +228,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[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.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
}; };
@@ -15,5 +15,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Camunda]: "Camunda", [AppConnection.Camunda]: "Camunda",
[AppConnection.Windmill]: "Windmill", [AppConnection.Windmill]: "Windmill",
[AppConnection.Auth0]: "Auth0", [AppConnection.Auth0]: "Auth0",
[AppConnection.LDAP]: "LDAP" [AppConnection.LDAP]: "LDAP",
[AppConnection.TeamCity]: "TeamCity"
}; };
@@ -46,6 +46,8 @@ import { humanitecConnectionService } from "./humanitec/humanitec-connection-ser
import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; 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";
@@ -76,7 +78,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema, [AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema, [AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema, [AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema [AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
}; };
export const appConnectionServiceFactory = ({ export const appConnectionServiceFactory = ({
@@ -452,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)
}; };
}; };
@@ -69,6 +69,12 @@ import {
TPostgresConnectionInput, TPostgresConnectionInput,
TValidatePostgresConnectionCredentialsSchema TValidatePostgresConnectionCredentialsSchema
} from "./postgres"; } from "./postgres";
import {
TTeamCityConnection,
TTeamCityConnectionConfig,
TTeamCityConnectionInput,
TValidateTeamCityConnectionCredentialsSchema
} from "./teamcity";
import { import {
TTerraformCloudConnection, TTerraformCloudConnection,
TTerraformCloudConnectionConfig, TTerraformCloudConnectionConfig,
@@ -104,6 +110,7 @@ export type TAppConnection = { id: string } & (
| TWindmillConnection | TWindmillConnection
| TAuth0Connection | TAuth0Connection
| TLdapConnection | TLdapConnection
| TTeamCityConnection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -126,6 +133,7 @@ export type TAppConnectionInput = { id: string } & (
| TWindmillConnectionInput | TWindmillConnectionInput
| TAuth0ConnectionInput | TAuth0ConnectionInput
| TLdapConnectionInput | TLdapConnectionInput
| TTeamCityConnectionInput
); );
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
@@ -153,7 +161,8 @@ export type TAppConnectionConfig =
| TCamundaConnectionConfig | TCamundaConnectionConfig
| TWindmillConnectionConfig | TWindmillConnectionConfig
| TAuth0ConnectionConfig | TAuth0ConnectionConfig
| TLdapConnectionConfig; | TLdapConnectionConfig
| TTeamCityConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@@ -170,7 +179,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateVercelConnectionCredentialsSchema | TValidateVercelConnectionCredentialsSchema
| TValidateWindmillConnectionCredentialsSchema | TValidateWindmillConnectionCredentialsSchema
| TValidateAuth0ConnectionCredentialsSchema | TValidateAuth0ConnectionCredentialsSchema
| TValidateLdapConnectionCredentialsSchema; | TValidateLdapConnectionCredentialsSchema
| TValidateTeamCityConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;
@@ -178,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
}; };
}; };
@@ -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,
@@ -100,7 +97,7 @@ export const identityAwsAuthServiceFactory = ({
const region = headers.Authorization ? awsRegionFromHeader(headers.Authorization) : null; const region = headers.Authorization ? awsRegionFromHeader(headers.Authorization) : null;
if (!isValidAwsRegion(region)) { if (!isValidAwsRegion(region)) {
throw new BadRequestError({message: "Invalid AWS region"}); throw new BadRequestError({ message: "Invalid AWS region" });
} }
const url = region ? `https://sts.${region}.amazonaws.com` : identityAwsAuth.stsEndpoint; const url = region ? `https://sts.${region}.amazonaws.com` : identityAwsAuth.stsEndpoint;
@@ -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,
+7 -1
View File
@@ -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({
+40 -2
View File
@@ -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"
+129 -2
View File
@@ -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)) {
secretsToUpdate.push(secret); // Only update secrets if the source value is not empty
if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination) importedSecretMap[key] = secretData; if (value) {
secretsToUpdate.push(secret);
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;
@@ -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
};
@@ -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,21 +915,22 @@ export const secretV2BridgeServiceFactory = ({
return decryptedSecrets; return decryptedSecrets;
}; };
const getSecrets = async ({ const getSecrets = async (dto: TGetSecretsDTO) => {
actorId, const {
path, actorId,
environment, path,
projectId, environment,
actor, projectId,
actorOrgId, actor,
viewSecretValue, actorOrgId,
actorAuthMethod, viewSecretValue,
includeImports, actorAuthMethod,
recursive, includeImports,
expandSecretReferences: shouldExpandSecretReferences, recursive,
throwOnMissingReadValuePermission = true, expandSecretReferences: shouldExpandSecretReferences,
...params throwOnMissingReadValuePermission = true,
}: TGetSecretsDTO) => { ...params
} = 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,
+1 -2
View File
@@ -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>
+152 -52
View File
@@ -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,25 +608,82 @@ func signKey(cmd *cobra.Command, args []string) {
} }
func sshConnect(cmd *cobra.Command, args []string) { func sshConnect(cmd *cobra.Command, args []string) {
util.RequireLogin() token, err := util.GetInfisicalToken(cmd)
util.RequireLocalWorkspaceFile()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil { if err != nil {
util.HandleError(err, "Unable to authenticate") util.HandleError(err, "Unable to parse flag")
} }
var infisicalToken string
if loggedInUserDetails.LoginExpired { if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") infisicalToken = token.Token
} else {
util.RequireLogin()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err, "Unable to authenticate")
}
if loggedInUserDetails.LoginExpired {
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,43 +706,68 @@ 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]
hostNames := make([]string, len(hosts)) if hostname != "" {
for i, h := range hosts { foundHost := false
hostNames[i] = h.Hostname 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))
for i, h := range hosts {
hostNames[i] = h.Hostname
}
hostPrompt := promptui.Select{
Label: "Select an SSH Host",
Items: hostNames,
Size: 10,
}
hostIdx, _, err := hostPrompt.Run()
if err != nil {
util.HandleError(err, "Prompt failed")
}
selectedHost = hosts[hostIdx]
} }
hostPrompt := promptui.Select{ var selectedLoginUser string
Label: "Select an SSH Host", if loginUser != "" {
Items: hostNames, foundLoginUser := false
Size: 10, 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 {
util.PrintErrorMessageAndExit("No login users available for selected host")
}
loginUsers := make([]string, len(selectedHost.LoginMappings))
for i, m := range selectedHost.LoginMappings {
loginUsers[i] = m.LoginUser
}
loginPrompt := promptui.Select{
Label: "Select Login User",
Items: loginUsers,
Size: 5,
}
loginIdx, _, err := loginPrompt.Run()
if err != nil {
util.HandleError(err, "Prompt failed")
}
selectedLoginUser = selectedHost.LoginMappings[loginIdx].LoginUser
} }
hostIdx, _, err := hostPrompt.Run()
if err != nil {
util.HandleError(err, "Prompt failed")
}
selectedHost := hosts[hostIdx]
// Prompt to select login user
if len(selectedHost.LoginMappings) == 0 {
util.PrintErrorMessageAndExit("No login users available for selected host")
}
loginUsers := make([]string, len(selectedHost.LoginMappings))
for i, m := range selectedHost.LoginMappings {
loginUsers[i] = m.LoginUser
}
loginPrompt := promptui.Select{
Label: "Select Login User",
Items: loginUsers,
Size: 5,
}
loginIdx, _, err := loginPrompt.Run()
if err != nil {
util.HandleError(err, "Prompt failed")
}
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)
+56
View File
@@ -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/teamcity/available"
---
@@ -0,0 +1,9 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/teamcity"
---
<Note>
Check out the configuration docs for [TeamCity Connections](/integrations/app-connections/teamcity) to learn how to obtain
the required credentials.
</Note>
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/teamcity/{connectionId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/teamcity/{connectionId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/teamcity/connection-name/{connectionName}"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/teamcity"
---
@@ -0,0 +1,9 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/teamcity/{connectionId}"
---
<Note>
Check out the configuration docs for [TeamCity Connections](/integrations/app-connections/teamcity) to learn how to obtain
the required credentials.
</Note>
@@ -0,0 +1,9 @@
---
title: "Create"
openapi: "POST /api/v2/secret-rotations/aws-iam-user-secret"
---
<Note>
Check out the configuration docs for [AWS IAM User Secret Rotations](/documentation/platform/secret-rotation/aws-iam-user-secret) to learn how to obtain the
required parameters.
</Note>
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret/rotation-name/{rotationName}"
---
@@ -0,0 +1,4 @@
---
title: "Get Credentials by ID"
openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}/generated-credentials"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret"
---
@@ -0,0 +1,4 @@
---
title: "Rotate Secrets"
openapi: "POST /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}/rotate-secrets"
---
@@ -0,0 +1,9 @@
---
title: "Update"
openapi: "PATCH /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}"
---
<Note>
Check out the configuration docs for [AWS IAM User Secret Rotations](/documentation/platform/secret-rotation/aws-iam-user-secret) to learn how to obtain the
required parameters.
</Note>
@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/teamcity"
---
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/teamcity/{syncId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/teamcity/{syncId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/teamcity/sync-name/{syncName}"
---
@@ -0,0 +1,4 @@
---
title: "Import Secrets"
openapi: "POST /api/v1/secret-syncs/teamcity/{syncId}/import-secrets"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/secret-syncs/teamcity"
---
@@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/teamcity/{syncId}/remove-secrets"
---
@@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/teamcity/{syncId}/sync-secrets"
---

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