Compare commits

..

82 Commits

Author SHA1 Message Date
632572f7c3 Merge pull request #3452 from Infisical/ldaps-connection-and-password-rotation
Feature: LDAP Connection and Password Rotation
2025-04-26 09:13:08 -07:00
a524690d01 deconflict merge 2025-04-25 17:20:30 -07:00
f93edbb37f Merge pull request #3493 from Infisical/improve-aws-connection-error-propagation
improvement(app-connections): Improve AWS Connection Error Propagation
2025-04-25 15:25:55 -07:00
fa8154ecdd improvement: add undefined handling 2025-04-25 15:06:16 -07:00
d977092502 improvement: improve validate aws connection error propagation 2025-04-25 15:05:22 -07:00
cceb29b93a Merge pull request #3476 from Infisical/ENG-2625
feat(secret-sync): TeamCity App Connection & Secret Sync
2025-04-25 15:44:37 -04:00
02b44365f1 Merge pull request #3470 from Infisical/feat/awsSecretRotationV2
feat(secret-rotation-v2): Add AWS IAM User Secret rotation
2025-04-25 16:43:22 -03:00
b506393765 feat(aws-iam-rotation): docs improvements 2025-04-25 16:35:57 -03:00
204269a10d Merge pull request #3480 from Infisical/feat/paginationAndFilterOnProjectMembers
feat(project-members): Persist pagination setting and add role filtering
2025-04-25 14:51:05 -03:00
cf1f83aaa3 Merge pull request #3446 from Infisical/ssh-non-interactive
Improvements to Infisical V2: Support for Non-Interactive Mode, Updating Default SSH CAs.
2025-04-25 10:15:06 -07:00
7894181234 Merge pull request #3490 from Infisical/ENG-2546
feat(auth): Persist pre-login-redirect path and redirect after login
2025-04-25 13:12:46 -04:00
0c214a2f26 Adjust CLI flags to be dash-case 2025-04-25 10:03:51 -07:00
f5862cbb9a Merge 2025-04-25 09:32:48 -07:00
bb699ecb5f Merge remote-tracking branch 'origin' into ssh-non-interactive 2025-04-25 09:31:39 -07:00
x
04b20ed11d feat(auth): Persist pre-login-redirect path and redirect after login 2025-04-25 12:09:18 -04:00
cd1e2af9bf Merge pull request #3489 from Infisical/feat/add-user-get-token-and-revamp-session-management
feat: add user get token CLI and revamp session management
2025-04-25 23:45:38 +08:00
7a4a877e39 feat(aws-iam-rotation): remove credentials validation due to excesive await time 2025-04-25 12:38:41 -03:00
8f670bde88 feat(aws-iam-rotation): add credentials validation 2025-04-25 12:06:30 -03:00
ff9011c899 feat(aws-iam-rotation): add view credentials component 2025-04-25 11:23:43 -03:00
57c96abe03 feat(aws-iam-rotation): address PR comments 2025-04-25 11:01:35 -03:00
178acc412d misc: added optional accesS 2025-04-25 20:52:55 +08:00
b0288c49c0 feat: add user get token CLI and revamp session management 2025-04-25 20:43:20 +08:00
f5bb0d4a86 Merge pull request #3484 from Infisical/fix/dynamicSecretSqlErrorPropagation
fix(dynamic-secret): improve error propagation and add FAQ to docs
2025-04-25 08:41:42 -03:00
x
7699705334 tiny encodeURIComponent tweak 2025-04-24 23:36:11 -04:00
x
7c49f6e302 review fixes 2025-04-24 23:30:35 -04:00
b329b5aa4b improvements: address feedback 2025-04-24 19:35:56 -07:00
x
0882c181d0 docs(native-integrations): Add deprication warnings on Windmill + TeamCity 2025-04-24 21:55:44 -04:00
x
8672dd641a Merge branch 'main' into ENG-2625 2025-04-24 21:26:05 -04:00
c613bb642e Merge pull request #3485 from Infisical/daniel/kms-logs
fix(kms): better error logs
2025-04-24 17:06:01 -07:00
90fdba0b77 Update kms-service.ts 2025-04-25 04:04:26 +04:00
795ce11062 Update kms-service.ts 2025-04-25 04:00:14 +04:00
2d4adfc651 fix(kms): better error logs 2025-04-25 03:54:59 +04:00
cb826f1a77 fix(dynamic-secret): improve error propagation and add FAQ to docs 2025-04-24 19:21:30 -03:00
55f6a06440 Merge pull request #2718 from akhilmhdh/doc/infisical-package
docs: added new docs for infisical package installation instructions
2025-04-24 14:18:07 -07:00
a19e5ff905 add min version 2025-04-24 14:16:56 -07:00
dccada8a12 Update docs/self-hosting/deployment-options/native/linux-package/installation.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-04-24 14:13:59 -07:00
68bbff455f Update docs/self-hosting/overview.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-04-24 14:12:59 -07:00
fcb59a1482 Update docs/self-hosting/overview.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-04-24 14:12:45 -07:00
b92bc2183a Update docs/self-hosting/deployment-options/native/linux-package/commands-configuration.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-04-24 14:12:27 -07:00
aff318cf3c Merge branch 'main' into doc/infisical-package 2025-04-24 14:12:01 -07:00
c97a3f07a7 update linux docs 2025-04-24 14:10:21 -07:00
e0dc2dd6d8 improvements: address feedback 2025-04-24 13:44:43 -07:00
8bf5b0f457 Merge pull request #3481 from Infisical/fix/AddDeleteProjectProtectedTooltip
fix(delete-project): Add tooltip for delete project button when it has protection enabled
2025-04-24 12:59:35 -03:00
4973447676 feat(project-members): PR suggestions improvements 2025-04-24 12:21:19 -03:00
bd2e2b7931 feat(project-members): PR suggestions improvements 2025-04-24 12:14:06 -03:00
13b7729af8 Merge pull request #3472 from Infisical/ENG-2618
Admin SSO bypass (break-glass login) sends out email to all org admins + creates audit log
2025-04-24 10:37:00 -04:00
x
e25c1199bc Made email URL use SITE_URL 2025-04-24 10:24:42 -04:00
6b3726957a Merge pull request #3443 from akhilmhdh/doc/sql-change
Updated doc to have europe infisical aws account id
2025-04-24 19:07:43 +05:30
c64e6310a6 fix(delete-project): Add tooltip for delete project button when it has protection enabled 2025-04-24 10:26:54 -03:00
aa893a40a9 feat(project-members): Persist pagination setting and add role filtering 2025-04-24 10:06:09 -03:00
0e488d840f Merge pull request #3479 from Infisical/update-org-structure-blueprint
Update the organization structure guide to include organizations and …
2025-04-23 21:18:43 -07:00
71258b6ea7 Merge pull request #3477 from Infisical/native-integration-deleted-import-fix
Fix: Filter Out Deleted Imports with Replication
2025-04-23 17:22:04 -07:00
49c90c801e fix: filter out deleted imports with replication 2025-04-23 17:03:33 -07:00
x
d019011822 Made findOrgMembersByUsername use replicaNode to stay consistent 2025-04-23 19:53:14 -04:00
x
8bd21ffa63 Attached settings URL to email, actor no longer a recipient, removed error handling for email send, used read replica node for findOrgMembersByRole 2025-04-23 19:46:25 -04:00
x
23df78eff8 feat(secret-sync): Only import secrets that have a value from destination to infisical: 2025-04-23 18:57:08 -04:00
x
84255d1b26 remove debug logs, update comments, other nitpicks 2025-04-23 18:44:14 -04:00
x
3a6b2a593b Merge branch 'main' into ENG-2625 2025-04-23 17:59:34 -04:00
x
d3ee30f5e6 feat(secret-sync): TeamCity App Connection & Secret Sync 2025-04-23 17:58:59 -04:00
33dea34061 chore: removed unused pick 2025-04-22 18:51:40 -07:00
da68073e86 chore: revert secret rotation flag 2025-04-22 18:06:44 -07:00
7bd312a287 improvements: update regex checks 2025-04-22 17:57:59 -07:00
d61e6752d6 Merge branch 'main' into ldaps-connection-and-password-rotation 2025-04-22 17:42:48 -07:00
636aee2ea9 improvements: address feedback 2025-04-22 17:36:18 -07:00
5819b8c576 PR fix suggestions for aws secret rotations 2025-04-22 17:40:15 -03:00
x
a838f84601 Revert license overwrites, fix type errors, add error handling to email function 2025-04-22 14:58:17 -04:00
x
a32b590dc5 Merge branch 'main' into ENG-2618 2025-04-22 14:37:22 -04:00
x
b330fdbc58 Admin SSO bypass (breakglass login) sends out email to all org admins + creates audit log 2025-04-22 14:36:31 -04:00
b85809293c Lint fix 2025-04-22 13:53:56 -03:00
f143d8c358 Merge branch 'main' into feat/awsSecretRotationV2 2025-04-22 13:46:35 -03:00
2e3330bf69 Add AWS secret rotation V2 2025-04-22 13:26:48 -03:00
9032bbe514 feature: ldap connection and password rotation 2025-04-18 17:55:03 -07:00
1ea8e5a81e Add frontend uniqueness check for ssh hostnames 2025-04-18 15:25:13 -07:00
42aa3c3d46 Remove extra tx in ssh nullable ca defaults migration, update ssh docs 2025-04-18 11:06:59 -07:00
184d353de5 Update infisical ssh docs to clarify ssh connect command in different modes 2025-04-17 23:29:20 -07:00
b2360f9cc8 Reuse writeToFile fn in ssh connect command 2025-04-17 23:12:44 -07:00
846a5a6e19 impl improvements according to greptile 2025-04-17 23:08:33 -07:00
c6cd3a8cc0 Add audit logs to project ssh config endpoints 2025-04-17 23:00:46 -07:00
796f5510ca Add cli docs for infisical ssh connect command 2025-04-17 22:40:43 -07:00
0265665e83 Make infisical ssh v2 work in non-interactive mode, allow reassignment of default ssh cas 2025-04-17 22:35:25 -07:00
=
79e425d807 feat: updated doc to have europe infisical aws account id 2025-04-17 14:25:55 +05:30
=
c1570930a9 docs: added new docs for infisical package installation instructions 2024-11-11 19:23:31 +05:30
262 changed files with 6250 additions and 581 deletions

View File

@ -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> {}

View File

@ -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
});

View File

@ -1,6 +1,8 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router";
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
@ -12,5 +14,7 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
> = {
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter
};

View File

@ -0,0 +1,19 @@
import {
CreateLdapPasswordRotationSchema,
LdapPasswordRotationGeneratedCredentialsSchema,
LdapPasswordRotationSchema,
UpdateLdapPasswordRotationSchema
} from "@app/ee/services/secret-rotation-v2/ldap-password";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
export const registerLdapPasswordRotationRouter = async (server: FastifyZodProvider) =>
registerSecretRotationEndpoints({
type: SecretRotation.LdapPassword,
server,
responseSchema: LdapPasswordRotationSchema,
createSchema: CreateLdapPasswordRotationSchema,
updateSchema: UpdateLdapPasswordRotationSchema,
generatedCredentialsSchema: LdapPasswordRotationGeneratedCredentialsSchema
});

View File

@ -2,6 +2,8 @@ import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
@ -13,7 +15,9 @@ import { AuthMode } from "@app/services/auth/auth-type";
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
PostgresCredentialsRotationListItemSchema,
MsSqlCredentialsRotationListItemSchema,
Auth0ClientSecretRotationListItemSchema
Auth0ClientSecretRotationListItemSchema,
LdapPasswordRotationListItemSchema,
AwsIamUserSecretRotationListItemSchema
]);
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {

View File

@ -234,6 +234,7 @@ export enum EventType {
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
@ -248,6 +249,8 @@ export enum EventType {
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-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",
CREATE_CMEK = "create-cmek",
UPDATE_CMEK = "update-cmek",
@ -1907,6 +1910,11 @@ interface OrgAdminAccessProjectEvent {
}; // no metadata yet
}
interface OrgAdminBypassSSOEvent {
type: EventType.ORG_ADMIN_BYPASS_SSO;
metadata: Record<string, string>; // no metadata yet
}
interface CreateCertificateTemplateEstConfig {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
@ -1986,6 +1994,25 @@ interface GetProjectSlackConfig {
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 {
type: EventType.INTEGRATION_SYNCED;
metadata: {
@ -2656,6 +2683,7 @@ export type Event =
| GetProjectKmsBackupEvent
| LoadProjectKmsBackupEvent
| OrgAdminAccessProjectEvent
| OrgAdminBypassSSOEvent
| CreateCertificateTemplate
| UpdateCertificateTemplate
| GetCertificateTemplate
@ -2670,6 +2698,8 @@ export type Event =
| GetSlackIntegration
| UpdateProjectSlackConfig
| GetProjectSlackConfig
| GetProjectSshConfig
| UpdateProjectSshConfig
| IntegrationSyncedEvent
| CreateCmekEvent
| UpdateCmekEvent

View File

@ -130,7 +130,17 @@ export const dynamicSecretLeaseServiceFactory = ({
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({
expireAt,
version: 1,

View File

@ -965,7 +965,6 @@ const buildMemberPermissionRules = () => {
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateAuthorities);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
@ -1031,7 +1030,6 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);

View File

@ -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"
}
}
};

View File

@ -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
};
};

View File

@ -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
});

View File

@ -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
>;

View File

@ -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";

View File

@ -0,0 +1,3 @@
export * from "./ldap-password-rotation-constants";
export * from "./ldap-password-rotation-schemas";
export * from "./ldap-password-rotation-types";

View File

@ -0,0 +1,15 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const LDAP_PASSWORD_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
name: "LDAP Password",
type: SecretRotation.LdapPassword,
connection: AppConnection.LDAP,
template: {
secretsMapping: {
dn: "LDAP_DN",
password: "LDAP_PASSWORD"
}
}
};

View File

@ -0,0 +1,181 @@
import ldap from "ldapjs";
import {
TRotationFactory,
TRotationFactoryGetSecretsPayload,
TRotationFactoryIssueCredentials,
TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { logger } from "@app/lib/logger";
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap";
import { generatePassword } from "../shared/utils";
import {
TLdapPasswordRotationGeneratedCredentials,
TLdapPasswordRotationWithConnection
} from "./ldap-password-rotation-types";
const getEncodedPassword = (password: string) => Buffer.from(`"${password}"`, "utf16le");
export const ldapPasswordRotationFactory: TRotationFactory<
TLdapPasswordRotationWithConnection,
TLdapPasswordRotationGeneratedCredentials
> = (secretRotation, appConnectionDAL, kmsService) => {
const {
connection,
parameters: { dn, passwordRequirements },
secretsMapping
} = secretRotation;
const $verifyCredentials = async (credentials: Pick<TLdapConnection["credentials"], "dn" | "password">) => {
try {
const client = await getLdapConnectionClient({ ...connection.credentials, ...credentials });
client.unbind();
client.destroy();
} catch (error) {
throw new Error(`Failed to verify credentials - ${(error as Error).message}`);
}
};
const $rotatePassword = async () => {
const { credentials, orgId } = connection;
if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection");
const client = await getLdapConnectionClient(credentials);
const isPersonalRotation = credentials.dn === dn;
const password = generatePassword(passwordRequirements);
let changes: ldap.Change[] | ldap.Change;
switch (credentials.provider) {
case LdapProvider.ActiveDirectory:
{
const encodedPassword = getEncodedPassword(password);
// service account vs personal password rotation require different changes
if (isPersonalRotation) {
const currentEncodedPassword = getEncodedPassword(credentials.password);
changes = [
new ldap.Change({
operation: "delete",
modification: {
type: "unicodePwd",
values: [currentEncodedPassword]
}
}),
new ldap.Change({
operation: "add",
modification: {
type: "unicodePwd",
values: [encodedPassword]
}
})
];
} else {
changes = new ldap.Change({
operation: "replace",
modification: {
type: "unicodePwd",
values: [encodedPassword]
}
});
}
}
break;
default:
throw new Error(`Unhandled provider: ${credentials.provider as LdapProvider}`);
}
try {
await new Promise((resolve, reject) => {
client.modify(dn, changes, (err) => {
if (err) {
logger.error(err, "LDAP Password Rotation Failed");
reject(new Error(`Provider Modify Error: ${err.message}`));
} else {
resolve(true);
}
});
});
} finally {
client.unbind();
client.destroy();
}
await $verifyCredentials({ dn, password });
if (isPersonalRotation) {
const updatedCredentials: TLdapConnection["credentials"] = {
...credentials,
password
};
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: updatedCredentials,
orgId,
kmsService
});
await appConnectionDAL.updateById(connection.id, { encryptedCredentials });
}
return { dn, password };
};
const issueCredentials: TRotationFactoryIssueCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
callback
) => {
const credentials = await $rotatePassword();
return callback(credentials);
};
const revokeCredentials: TRotationFactoryRevokeCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
_,
callback
) => {
// we just rotate to a new password, essentially revoking old credentials
await $rotatePassword();
return callback();
};
const rotateCredentials: TRotationFactoryRotateCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
_,
callback
) => {
const credentials = await $rotatePassword();
return callback(credentials);
};
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TLdapPasswordRotationGeneratedCredentials> = (
generatedCredentials
) => {
const secrets = [
{
key: secretsMapping.dn,
value: generatedCredentials.dn
},
{
key: secretsMapping.password,
value: generatedCredentials.password
}
];
return secrets;
};
return {
issueCredentials,
revokeCredentials,
rotateCredentials,
getSecretsPayload
};
};

View File

@ -0,0 +1,68 @@
import RE2 from "re2";
import { z } from "zod";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
BaseCreateSecretRotationSchema,
BaseSecretRotationSchema,
BaseUpdateSecretRotationSchema
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
import { PasswordRequirementsSchema } from "@app/ee/services/secret-rotation-v2/shared/general";
import { SecretRotations } from "@app/lib/api-docs";
import { DistinguishedNameRegex } from "@app/lib/regex";
import { SecretNameSchema } from "@app/server/lib/schemas";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const LdapPasswordRotationGeneratedCredentialsSchema = z
.object({
dn: z.string(),
password: z.string()
})
.array()
.min(1)
.max(2);
const LdapPasswordRotationParametersSchema = z.object({
dn: z
.string()
.trim()
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com")
.min(1, "Distinguished Name (DN) Required")
.describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.dn),
passwordRequirements: PasswordRequirementsSchema.optional()
});
const LdapPasswordRotationSecretsMappingSchema = z.object({
dn: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.dn),
password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.password)
});
export const LdapPasswordRotationTemplateSchema = z.object({
secretsMapping: z.object({
dn: z.string(),
password: z.string()
})
});
export const LdapPasswordRotationSchema = BaseSecretRotationSchema(SecretRotation.LdapPassword).extend({
type: z.literal(SecretRotation.LdapPassword),
parameters: LdapPasswordRotationParametersSchema,
secretsMapping: LdapPasswordRotationSecretsMappingSchema
});
export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword).extend({
parameters: LdapPasswordRotationParametersSchema,
secretsMapping: LdapPasswordRotationSecretsMappingSchema
});
export const UpdateLdapPasswordRotationSchema = BaseUpdateSecretRotationSchema(SecretRotation.LdapPassword).extend({
parameters: LdapPasswordRotationParametersSchema.optional(),
secretsMapping: LdapPasswordRotationSecretsMappingSchema.optional()
});
export const LdapPasswordRotationListItemSchema = z.object({
name: z.literal("LDAP Password"),
connection: z.literal(AppConnection.LDAP),
type: z.literal(SecretRotation.LdapPassword),
template: LdapPasswordRotationTemplateSchema
});

View File

@ -0,0 +1,22 @@
import { z } from "zod";
import { TLdapConnection } from "@app/services/app-connection/ldap";
import {
CreateLdapPasswordRotationSchema,
LdapPasswordRotationGeneratedCredentialsSchema,
LdapPasswordRotationListItemSchema,
LdapPasswordRotationSchema
} from "./ldap-password-rotation-schemas";
export type TLdapPasswordRotation = z.infer<typeof LdapPasswordRotationSchema>;
export type TLdapPasswordRotationInput = z.infer<typeof CreateLdapPasswordRotationSchema>;
export type TLdapPasswordRotationListItem = z.infer<typeof LdapPasswordRotationListItemSchema>;
export type TLdapPasswordRotationWithConnection = TLdapPasswordRotation & {
connection: TLdapConnection;
};
export type TLdapPasswordRotationGeneratedCredentials = z.infer<typeof LdapPasswordRotationGeneratedCredentialsSchema>;

View File

@ -1,7 +1,9 @@
export enum SecretRotation {
PostgresCredentials = "postgres-credentials",
MsSqlCredentials = "mssql-credentials",
Auth0ClientSecret = "auth0-client-secret"
Auth0ClientSecret = "auth0-client-secret",
LdapPassword = "ldap-password",
AwsIamUserSecret = "aws-iam-user-secret"
}
export enum SecretRotationStatus {

View File

@ -4,6 +4,8 @@ import { getConfig } from "@app/lib/config/env";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret";
import { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password";
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
@ -18,7 +20,9 @@ import {
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION
};
export const listSecretRotationOptions = () => {

View File

@ -3,12 +3,16 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials",
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret"
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials",
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
[SecretRotation.LdapPassword]: "LDAP Password",
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret"
};
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
[SecretRotation.LdapPassword]: AppConnection.LDAP,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
};

View File

@ -14,6 +14,7 @@ import {
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns";
import { ldapPasswordRotationFactory } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns";
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
calculateNextRotationAt,
@ -77,6 +78,7 @@ import {
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-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";
export type TSecretRotationV2ServiceFactoryDep = {
@ -114,7 +116,9 @@ type TRotationFactoryImplementation = TRotationFactory<
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation
};
export const secretRotationV2ServiceFactory = ({
@ -449,6 +453,18 @@ export const secretRotationV2ServiceFactory = ({
kmsService
);
// even though we have a db constraint we want to check before any rotation of credentials is attempted
// to prevent creation failure after external credentials have been modified
const conflictingRotation = await secretRotationV2DAL.findOne({
name: payload.name,
folderId: folder.id
});
if (conflictingRotation)
throw new BadRequestError({
message: `A Secret Rotation with the name "${payload.name}" already exists at the secret path "${secretPath}"`
});
try {
const currentTime = new Date();

View File

@ -12,6 +12,20 @@ import {
TAuth0ClientSecretRotationListItem,
TAuth0ClientSecretRotationWithConnection
} from "./auth0-client-secret";
import {
TAwsIamUserSecretRotation,
TAwsIamUserSecretRotationGeneratedCredentials,
TAwsIamUserSecretRotationInput,
TAwsIamUserSecretRotationListItem,
TAwsIamUserSecretRotationWithConnection
} from "./aws-iam-user-secret";
import {
TLdapPasswordRotation,
TLdapPasswordRotationGeneratedCredentials,
TLdapPasswordRotationInput,
TLdapPasswordRotationListItem,
TLdapPasswordRotationWithConnection
} from "./ldap-password";
import {
TMsSqlCredentialsRotation,
TMsSqlCredentialsRotationInput,
@ -27,26 +41,39 @@ import {
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
import { SecretRotation } from "./secret-rotation-v2-enums";
export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation;
export type TSecretRotationV2 =
| TPostgresCredentialsRotation
| TMsSqlCredentialsRotation
| TAuth0ClientSecretRotation
| TLdapPasswordRotation
| TAwsIamUserSecretRotation;
export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection
| TMsSqlCredentialsRotationWithConnection
| TAuth0ClientSecretRotationWithConnection;
| TAuth0ClientSecretRotationWithConnection
| TLdapPasswordRotationWithConnection
| TAwsIamUserSecretRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials
| TAuth0ClientSecretRotationGeneratedCredentials;
| TAuth0ClientSecretRotationGeneratedCredentials
| TLdapPasswordRotationGeneratedCredentials
| TAwsIamUserSecretRotationGeneratedCredentials;
export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput
| TMsSqlCredentialsRotationInput
| TAuth0ClientSecretRotationInput;
| TAuth0ClientSecretRotationInput
| TLdapPasswordRotationInput
| TAwsIamUserSecretRotationInput;
export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem
| TMsSqlCredentialsRotationListItem
| TAuth0ClientSecretRotationListItem;
| TAuth0ClientSecretRotationListItem
| TLdapPasswordRotationListItem
| TAwsIamUserSecretRotationListItem;
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;

View File

@ -1,11 +1,16 @@
import { z } from "zod";
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { AwsIamUserSecretRotationSchema } from "./aws-iam-user-secret";
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
PostgresCredentialsRotationSchema,
MsSqlCredentialsRotationSchema,
Auth0ClientSecretRotationSchema
Auth0ClientSecretRotationSchema,
LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema
]);

View File

@ -0,0 +1 @@
export * from "./password-requirements-schema";

View File

@ -0,0 +1,44 @@
import RE2 from "re2";
import { z } from "zod";
import { SecretRotations } from "@app/lib/api-docs";
export const PasswordRequirementsSchema = z
.object({
length: z
.number()
.min(1, "Password length must be a positive number")
.max(250, "Password length must be less than 250")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.length),
required: z.object({
digits: z
.number()
.min(0, "Digit count must be non-negative")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.digits),
lowercase: z
.number()
.min(0, "Lowercase count must be non-negative")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.lowercase),
uppercase: z
.number()
.min(0, "Uppercase count must be non-negative")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.uppercase),
symbols: z
.number()
.min(0, "Symbol count must be non-negative")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.symbols)
}),
allowedSymbols: z
.string()
.regex(new RE2("[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?~]"), "Invalid symbols")
.optional()
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.allowedSymbols)
})
.refine((data) => {
return Object.values(data.required).some((count) => count > 0);
}, "At least one character type must be required")
.refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
return total <= data.length;
}, "Sum of required characters cannot exceed the total length")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.base);

View File

@ -1,6 +1,17 @@
import { randomInt } from "crypto";
const DEFAULT_PASSWORD_REQUIREMENTS = {
type TPasswordRequirements = {
length: number;
required: {
lowercase: number;
uppercase: number;
digits: number;
symbols: number;
};
allowedSymbols?: string;
};
const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
length: 48,
required: {
lowercase: 1,
@ -11,9 +22,9 @@ const DEFAULT_PASSWORD_REQUIREMENTS = {
allowedSymbols: "-_.~!*"
};
export const generatePassword = () => {
export const generatePassword = (passwordRequirements?: TPasswordRequirements) => {
try {
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
const { length, required, allowedSymbols } = passwordRequirements ?? DEFAULT_PASSWORD_REQUIREMENTS;
const chars = {
lowercase: "abcdefghijklmnopqrstuvwxyz",

View File

@ -1857,6 +1857,20 @@ export const AppConnections = {
WINDMILL: {
instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).",
accessToken: "The access token to use to connect with Windmill."
},
LDAP: {
provider: "The type of LDAP provider. Determines provider-specific behaviors.",
url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').",
dn: "The Distinguished Name (DN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com').",
password: "The password to bind with for authentication.",
sslRejectUnauthorized:
"Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.",
sslCertificate:
"The SSL certificate (PEM format) to use for secure connection when using ldaps:// with a self-signed certificate."
},
TEAMCITY: {
instanceUrl: "The TeamCity instance URL to connect with.",
accessToken: "The access token to use to connect with TeamCity."
}
}
};
@ -1996,6 +2010,10 @@ export const SecretSyncs = {
WINDMILL: {
workspace: "The Windmill workspace to sync secrets to.",
path: "The Windmill workspace path to sync secrets to."
},
TEAMCITY: {
project: "The TeamCity project to sync secrets to.",
buildConfig: "The TeamCity build configuration to sync secrets to."
}
}
};
@ -2060,6 +2078,26 @@ export const SecretRotations = {
},
AUTH0_CLIENT_SECRET: {
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
},
LDAP_PASSWORD: {
dn: "The Distinguished Name (DN) of the principal to rotate the password for."
},
GENERAL: {
PASSWORD_REQUIREMENTS: {
base: "The password requirements to use when generating the new password.",
length: "The length of the password to generate.",
required: {
digits: "The amount of digits to require in the generated password.",
lowercase: "The amount of lowercase characters to require in the generated password.",
uppercase: "The amount of uppercase characters to require in the generated password.",
symbols: "The amount of symbols to require in the generated password."
},
allowedSymbols: 'The allowed symbols to use in the generated password (defaults to "-_.~!*").'
}
},
AWS_IAM_USER_SECRET: {
userName: "The name of the client to rotate credentials for.",
region: "The AWS region the client is present in."
}
},
SECRETS_MAPPING: {
@ -2070,6 +2108,14 @@ export const SecretRotations = {
AUTH0_CLIENT_SECRET: {
clientId: "The name of the secret that the client ID will be mapped to.",
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
},
LDAP_PASSWORD: {
dn: "The name of the secret that the Distinguished Name (DN) of the principal will be mapped to.",
password: "The name of the secret that the rotated password will be mapped to."
},
AWS_IAM_USER_SECRET: {
accessKeyId: "The name of the secret that the access key ID will be mapped to.",
secretAccessKey: "The name of the secret that the rotated secret access key will be mapped to."
}
}
};

View File

@ -0,0 +1,3 @@
export const DistinguishedNameRegex =
// DN format, ie; CN=user,OU=users,DC=example,DC=com
/^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/;

View File

@ -596,7 +596,14 @@ export const registerRoutes = async (
kmsService
});
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, totpService });
const loginService = authLoginServiceFactory({
userDAL,
smtpService,
tokenService,
orgDAL,
totpService,
auditLogService
});
const passwordService = authPaswordServiceFactory({
tokenService,
smtpService,

View File

@ -28,11 +28,16 @@ import {
HumanitecConnectionListItemSchema,
SanitizedHumanitecConnectionSchema
} from "@app/services/app-connection/humanitec";
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import {
PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema
} from "@app/services/app-connection/postgres";
import {
SanitizedTeamCityConnectionSchema,
TeamCityConnectionListItemSchema
} from "@app/services/app-connection/teamcity";
import {
SanitizedTerraformCloudConnectionSchema,
TerraformCloudConnectionListItemSchema
@ -59,7 +64,9 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedMsSqlConnectionSchema.options,
...SanitizedCamundaConnectionSchema.options,
...SanitizedWindmillConnectionSchema.options,
...SanitizedAuth0ConnectionSchema.options
...SanitizedAuth0ConnectionSchema.options,
...SanitizedLdapConnectionSchema.options,
...SanitizedTeamCityConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@ -76,7 +83,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
MsSqlConnectionListItemSchema,
CamundaConnectionListItemSchema,
WindmillConnectionListItemSchema,
Auth0ConnectionListItemSchema
Auth0ConnectionListItemSchema,
LdapConnectionListItemSchema,
TeamCityConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@ -59,4 +59,40 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
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 };
}
});
};

View File

@ -1,6 +1,6 @@
import { registerAuth0ConnectionRouter } from "@app/server/routes/v1/app-connection-routers/auth0-connection-router";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
import { registerAwsConnectionRouter } from "./aws-connection-router";
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
@ -9,8 +9,10 @@ import { registerDatabricksConnectionRouter } from "./databricks-connection-rout
import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
import { registerVercelConnectionRouter } from "./vercel-connection-router";
import { registerWindmillConnectionRouter } from "./windmill-connection-router";
@ -32,5 +34,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
[AppConnection.Camunda]: registerCamundaConnectionRouter,
[AppConnection.Windmill]: registerWindmillConnectionRouter,
[AppConnection.Auth0]: registerAuth0ConnectionRouter
[AppConnection.Auth0]: registerAuth0ConnectionRouter,
[AppConnection.LDAP]: registerLdapConnectionRouter,
[AppConnection.TeamCity]: registerTeamCityConnectionRouter
};

View File

@ -0,0 +1,18 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateLdapConnectionSchema,
SanitizedLdapConnectionSchema,
UpdateLdapConnectionSchema
} from "@app/services/app-connection/ldap";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerLdapConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.LDAP,
server,
sanitizedResponseSchema: SanitizedLdapConnectionSchema,
createSchema: CreateLdapConnectionSchema,
updateSchema: UpdateLdapConnectionSchema
});
};

View File

@ -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;
}
});
};

View File

@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import {
@ -6,6 +7,7 @@ import {
ProjectMembershipsSchema,
ProjectRolesSchema,
ProjectSlackConfigsSchema,
ProjectSshConfigsSchema,
ProjectType,
SecretFoldersSchema,
SortDirection,
@ -78,7 +80,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
includeGroupMembers: z
.enum(["true", "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({
workspaceId: z.string().trim()
@ -117,13 +129,15 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const roles = (req.query.roles?.split(",") || []).filter(Boolean);
const users = await server.services.projectMembership.getProjectMemberships({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
includeGroupMembers: req.query.includeGroupMembers,
projectId: req.params.workspaceId,
actorOrgId: req.permission.orgId
actorOrgId: req.permission.orgId,
roles
});
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({
method: "GET",
url: "/:workspaceId/slack-config",

View File

@ -9,6 +9,7 @@ import { registerDatabricksSyncRouter } from "./databricks-sync-router";
import { registerGcpSyncRouter } from "./gcp-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-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.Camunda]: registerCamundaSyncRouter,
[SecretSync.Vercel]: registerVercelSyncRouter,
[SecretSync.Windmill]: registerWindmillSyncRouter
[SecretSync.Windmill]: registerWindmillSyncRouter,
[SecretSync.TeamCity]: registerTeamCitySyncRouter
};

View File

@ -23,6 +23,7 @@ import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/service
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
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 { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill";
@ -39,7 +40,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
TerraformCloudSyncSchema,
CamundaSyncSchema,
VercelSyncSchema,
WindmillSyncSchema
WindmillSyncSchema,
TeamCitySyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@ -54,7 +56,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
TerraformCloudSyncListItemSchema,
CamundaSyncListItemSchema,
VercelSyncListItemSchema,
WindmillSyncListItemSchema
WindmillSyncListItemSchema,
TeamCitySyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@ -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
});

View File

@ -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({
method: "GET",
url: "/me",

View File

@ -12,7 +12,9 @@ export enum AppConnection {
MsSql = "mssql",
Camunda = "camunda",
Windmill = "windmill",
Auth0 = "auth0"
Auth0 = "auth0",
LDAP = "ldap",
TeamCity = "teamcity"
}
export enum AWSRegion {

View File

@ -41,8 +41,14 @@ import {
HumanitecConnectionMethod,
validateHumanitecConnectionCredentials
} from "./humanitec";
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import {
getTeamCityConnectionListItem,
TeamCityConnectionMethod,
validateTeamCityConnectionCredentials
} from "./teamcity";
import {
getTerraformCloudConnectionListItem,
TerraformCloudConnectionMethod,
@ -71,7 +77,9 @@ export const listAppConnectionOptions = () => {
getMsSqlConnectionListItem(),
getCamundaConnectionListItem(),
getWindmillConnectionListItem(),
getAuth0ConnectionListItem()
getAuth0ConnectionListItem(),
getLdapConnectionListItem(),
getTeamCityConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@ -135,7 +143,9 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@ -167,9 +177,12 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case MsSqlConnectionMethod.UsernameAndPassword:
return "Username & Password";
case WindmillConnectionMethod.AccessToken:
case TeamCityConnectionMethod.AccessToken:
return "Access Token";
case Auth0ConnectionMethod.ClientCredentials:
return "Client Credentials";
case LdapConnectionMethod.SimpleBind:
return "Simple Bind";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);
@ -214,5 +227,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
};

View File

@ -14,5 +14,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.MsSql]: "Microsoft SQL Server",
[AppConnection.Camunda]: "Camunda",
[AppConnection.Windmill]: "Windmill",
[AppConnection.Auth0]: "Auth0"
[AppConnection.Auth0]: "Auth0",
[AppConnection.LDAP]: "LDAP",
[AppConnection.TeamCity]: "TeamCity"
};

View File

@ -43,8 +43,11 @@ import { ValidateGitHubConnectionCredentialsSchema } from "./github";
import { githubConnectionService } from "./github/github-connection-service";
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
@ -74,7 +77,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@ -450,6 +455,7 @@ export const appConnectionServiceFactory = ({
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
vercel: vercelConnectionService(connectAppConnectionById),
windmill: windmillConnectionService(connectAppConnectionById),
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
teamcity: teamcityConnectionService(connectAppConnectionById)
};
};

View File

@ -57,12 +57,24 @@ import {
THumanitecConnectionInput,
TValidateHumanitecConnectionCredentialsSchema
} from "./humanitec";
import {
TLdapConnection,
TLdapConnectionConfig,
TLdapConnectionInput,
TValidateLdapConnectionCredentialsSchema
} from "./ldap";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import {
TPostgresConnection,
TPostgresConnectionInput,
TValidatePostgresConnectionCredentialsSchema
} from "./postgres";
import {
TTeamCityConnection,
TTeamCityConnectionConfig,
TTeamCityConnectionInput,
TValidateTeamCityConnectionCredentialsSchema
} from "./teamcity";
import {
TTerraformCloudConnection,
TTerraformCloudConnectionConfig,
@ -97,6 +109,8 @@ export type TAppConnection = { id: string } & (
| TCamundaConnection
| TWindmillConnection
| TAuth0Connection
| TLdapConnection
| TTeamCityConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@ -118,6 +132,8 @@ export type TAppConnectionInput = { id: string } & (
| TCamundaConnectionInput
| TWindmillConnectionInput
| TAuth0ConnectionInput
| TLdapConnectionInput
| TTeamCityConnectionInput
);
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
@ -144,7 +160,9 @@ export type TAppConnectionConfig =
| TSqlConnectionConfig
| TCamundaConnectionConfig
| TWindmillConnectionConfig
| TAuth0ConnectionConfig;
| TAuth0ConnectionConfig
| TLdapConnectionConfig
| TTeamCityConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@ -160,7 +178,9 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateTerraformCloudConnectionCredentialsSchema
| TValidateVercelConnectionCredentialsSchema
| TValidateWindmillConnectionCredentialsSchema
| TValidateAuth0ConnectionCredentialsSchema;
| TValidateAuth0ConnectionCredentialsSchema
| TValidateLdapConnectionCredentialsSchema
| TValidateTeamCityConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
@ -168,6 +188,10 @@ export type TListAwsConnectionKmsKeys = {
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
};
export type TListAwsConnectionIamUsers = {
connectionId: string;
};
export type TAppConnectionCredentialsValidator = (
appConnection: TAppConnectionConfig
) => Promise<TAppConnection["credentials"]>;

View File

@ -1,9 +1,11 @@
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import AWS from "aws-sdk";
import { AxiosError } from "axios";
import { randomUUID } from "crypto";
import { getConfig } from "@app/lib/config/env";
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 { AwsConnectionMethod } from "./aws-connection-enums";
@ -90,9 +92,20 @@ export const validateAwsConnectionCredentials = async (appConnection: TAwsConnec
const sts = new AWS.STS(awsConfig);
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({
message: `Unable to validate connection: verify credentials`
message: `Unable to validate connection: ${message}`
});
}

View File

@ -2,7 +2,10 @@ import AWS from "aws-sdk";
import { OrgServiceActor } from "@app/lib/types";
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 { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
@ -70,6 +73,23 @@ const listAwsKmsKeys = async (
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) => {
const listKmsKeys = async (
{ connectionId, region, destination }: TListAwsConnectionKmsKeys,
@ -82,7 +102,16 @@ export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) =>
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 {
listKmsKeys
listKmsKeys,
listIamUsers
};
};

View File

@ -0,0 +1,4 @@
export * from "./ldap-connection-enums";
export * from "./ldap-connection-fns";
export * from "./ldap-connection-schemas";
export * from "./ldap-connection-types";

View File

@ -0,0 +1,7 @@
export enum LdapConnectionMethod {
SimpleBind = "simple-bind"
}
export enum LdapProvider {
ActiveDirectory = "active-directory"
}

View File

@ -0,0 +1,102 @@
import ldap from "ldapjs";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { LdapConnectionMethod } from "./ldap-connection-enums";
import { TLdapConnectionConfig } from "./ldap-connection-types";
export const getLdapConnectionListItem = () => {
return {
name: "LDAP" as const,
app: AppConnection.LDAP as const,
methods: Object.values(LdapConnectionMethod) as [LdapConnectionMethod.SimpleBind]
};
};
const LDAP_TIMEOUT = 15_000;
export const getLdapConnectionClient = async ({
url,
dn,
password,
sslCertificate,
sslRejectUnauthorized = true
}: TLdapConnectionConfig["credentials"]) => {
await blockLocalAndPrivateIpAddresses(url);
const isSSL = url.startsWith("ldaps");
return new Promise<ldap.Client>((resolve, reject) => {
const client = ldap.createClient({
url,
timeout: LDAP_TIMEOUT,
connectTimeout: LDAP_TIMEOUT,
tlsOptions: isSSL
? {
rejectUnauthorized: sslRejectUnauthorized,
ca: sslCertificate ? [sslCertificate] : undefined
}
: undefined
});
client.on("error", (err: Error) => {
logger.error(err, "LDAP Error");
client.destroy();
reject(new Error(`Provider Error - ${err.message}`));
});
client.on("connectError", (err: Error) => {
logger.error(err, "LDAP Connection Error");
client.destroy();
reject(new Error(`Provider Connect Error - ${err.message}`));
});
client.on("connectRefused", (err: Error) => {
logger.error(err, "LDAP Connection Refused");
client.destroy();
reject(new Error(`Provider Connection Refused - ${err.message}`));
});
client.on("connectTimeout", (err: Error) => {
logger.error(err, "LDAP Connection Timeout");
client.destroy();
reject(new Error(`Provider Connection Timeout - ${err.message}`));
});
client.on("connect", () => {
client.bind(dn, password, (err) => {
if (err) {
logger.error(err, "LDAP Bind Error");
reject(new Error(`Bind Error: ${err.message}`));
client.destroy();
}
resolve(client);
});
});
});
};
export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => {
let client: ldap.Client | undefined;
try {
client = await getLdapConnectionClient(credentials);
// this shouldn't occur as handle connection error events in client but here as fallback
if (!client.connected) {
throw new BadRequestError({ message: "Unable to connect to LDAP server" });
}
return credentials;
} catch (e: unknown) {
throw new BadRequestError({
message: `Unable to validate connection: ${(e as Error).message || "verify credentials"}`
});
} finally {
client?.destroy();
}
};

View File

@ -0,0 +1,93 @@
import RE2 from "re2";
import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { DistinguishedNameRegex } from "@app/lib/regex";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { LdapConnectionMethod, LdapProvider } from "./ldap-connection-enums";
export const LdapConnectionSimpleBindCredentialsSchema = z.object({
provider: z.nativeEnum(LdapProvider).describe(AppConnections.CREDENTIALS.LDAP.provider),
url: z
.string()
.trim()
.min(1, "URL required")
.regex(new RE2(/^ldaps?:\/\//))
.describe(AppConnections.CREDENTIALS.LDAP.url),
dn: z
.string()
.trim()
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com")
.min(1, "Distinguished Name (DN) required")
.describe(AppConnections.CREDENTIALS.LDAP.dn),
password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.LDAP.password),
sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.LDAP.sslRejectUnauthorized),
sslCertificate: z
.string()
.trim()
.transform((value) => value || undefined)
.optional()
.describe(AppConnections.CREDENTIALS.LDAP.sslCertificate)
});
const BaseLdapConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.LDAP)
});
export const LdapConnectionSchema = z.intersection(
BaseLdapConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(LdapConnectionMethod.SimpleBind),
credentials: LdapConnectionSimpleBindCredentialsSchema
})
])
);
export const SanitizedLdapConnectionSchema = z.discriminatedUnion("method", [
BaseLdapConnectionSchema.extend({
method: z.literal(LdapConnectionMethod.SimpleBind),
credentials: LdapConnectionSimpleBindCredentialsSchema.pick({
provider: true,
url: true,
dn: true,
sslRejectUnauthorized: true,
sslCertificate: true
})
})
]);
export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(LdapConnectionMethod.SimpleBind).describe(AppConnections.CREATE(AppConnection.LDAP).method),
credentials: LdapConnectionSimpleBindCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.LDAP).credentials
)
})
]);
export const CreateLdapConnectionSchema = ValidateLdapConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.LDAP)
);
export const UpdateLdapConnectionSchema = z
.object({
credentials: LdapConnectionSimpleBindCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.LDAP).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.LDAP));
export const LdapConnectionListItemSchema = z.object({
name: z.literal("LDAP"),
app: z.literal(AppConnection.LDAP),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(LdapConnectionMethod).array()
});

View File

@ -0,0 +1,22 @@
import { z } from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateLdapConnectionSchema,
LdapConnectionSchema,
ValidateLdapConnectionCredentialsSchema
} from "./ldap-connection-schemas";
export type TLdapConnection = z.infer<typeof LdapConnectionSchema>;
export type TLdapConnectionInput = z.infer<typeof CreateLdapConnectionSchema> & {
app: AppConnection.LDAP;
};
export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema;
export type TLdapConnectionConfig = DiscriminativePick<TLdapConnection, "method" | "app" | "credentials"> & {
orgId: string;
};

View File

@ -31,7 +31,8 @@ export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [
port: true,
username: true,
sslEnabled: true,
sslRejectUnauthorized: true
sslRejectUnauthorized: true,
sslCertificate: true
})
})
]);

View File

@ -29,7 +29,8 @@ export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method",
port: true,
username: true,
sslEnabled: true,
sslRejectUnauthorized: true
sslRejectUnauthorized: true,
sslCertificate: true
})
})
]);

View File

@ -0,0 +1,4 @@
export * from "./teamcity-connection-enums";
export * from "./teamcity-connection-fns";
export * from "./teamcity-connection-schemas";
export * from "./teamcity-connection-types";

View File

@ -0,0 +1,3 @@
export enum TeamCityConnectionMethod {
AccessToken = "access-token"
}

View File

@ -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");
};

View File

@ -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()
});

View File

@ -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
};
};

View File

@ -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[];
};

View File

@ -47,7 +47,10 @@ export const tokenDALFactory = (db: TDbClient) => {
const findTokenSessions = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
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;
} catch (error) {
throw new DatabaseError({ name: "Find all token session", error });

View File

@ -151,6 +151,9 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
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 appCfg = getConfig();
if (!refreshToken)
@ -223,6 +226,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
clearTokenSessionById,
getTokenSessionByUser,
revokeAllMySessions,
revokeMySessionById,
validateRefreshToken,
fnValidateJwtIdentity,
getUserTokenSessionById

View File

@ -3,6 +3,8 @@ import jwt from "jsonwebtoken";
import { Knex } from "knex";
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 { getConfig } from "@app/lib/config/env";
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 { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
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 { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
@ -28,7 +32,14 @@ import {
TOauthTokenExchangeDTO,
TVerifyMfaTokenDTO
} 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 = {
userDAL: TUserDALFactory;
@ -36,6 +47,7 @@ type TAuthLoginServiceFactoryDep = {
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
};
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
@ -44,7 +56,8 @@ export const authLoginServiceFactory = ({
tokenService,
smtpService,
orgDAL,
totpService
totpService,
auditLogService
}: TAuthLoginServiceFactoryDep) => {
/*
* Private
@ -412,6 +425,55 @@ export const authLoginServiceFactory = ({
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 {
...tokens,
isMfaEnabled: false

View File

@ -787,13 +787,19 @@ export const kmsServiceFactory = ({
return projectDataKey;
}
}
} catch (error) {
logger.error(
error,
`getProjectSecretManagerKmsDataKey: Failed to get project data key for [projectId=${projectId}]`
);
throw error;
} finally {
await lock?.release();
}
}
if (!project.kmsSecretManagerEncryptedDataKey) {
throw new Error("Missing project data key");
throw new BadRequestError({ message: "Missing project data key" });
}
const kmsDecryptor = await decryptWithKmsKey({

View File

@ -2,6 +2,7 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db";
import {
OrgMembershipRole,
TableName,
TOrganizations,
TOrganizationsInsert,
@ -216,9 +217,8 @@ export const orgDALFactory = (db: TDbClient) => {
const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => {
try {
const conn = tx || db;
const conn = tx || db.replicaNode();
const members = await conn(TableName.OrgMembership)
// .replicaNode()(TableName.OrgMembership)
.where(`${TableName.OrgMembership}.orgId`, orgId)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.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) => {
try {
const member = await db
@ -472,6 +509,7 @@ export const orgDALFactory = (db: TDbClient) => {
findAllOrgsByUserId,
ghostUserExists,
findOrgMembersByUsername,
findOrgMembersByRole,
findOrgGhostUser,
create,
updateById,

View File

@ -13,7 +13,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
// special query
const findAllProjectMembers = async (
projectId: string,
filter: { usernames?: string[]; username?: string; id?: string } = {}
filter: { usernames?: string[]; username?: string; id?: string; roles?: string[] } = {}
) => {
try {
const docs = await db
@ -31,6 +31,29 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
if (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>(
TableName.UserEncryptionKey,

View File

@ -79,7 +79,8 @@ export const projectMembershipServiceFactory = ({
actorOrgId,
actorAuthMethod,
includeGroupMembers,
projectId
projectId,
roles
}: TGetProjectMembershipDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
@ -91,7 +92,7 @@ export const projectMembershipServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId, { roles });
// projectMembers[0].project
if (includeGroupMembers) {

View File

@ -1,6 +1,6 @@
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 enum ProjectUserMembershipTemporaryMode {
Relative = "relative"

View File

@ -73,6 +73,7 @@ import {
TGetProjectDTO,
TGetProjectKmsKey,
TGetProjectSlackConfig,
TGetProjectSshConfig,
TListProjectAlertsDTO,
TListProjectCasDTO,
TListProjectCertificateTemplatesDTO,
@ -92,6 +93,7 @@ import {
TUpdateProjectKmsDTO,
TUpdateProjectNameDTO,
TUpdateProjectSlackConfig,
TUpdateProjectSshConfig,
TUpdateProjectVersionLimitDTO,
TUpgradeProjectDTO
} from "./project-types";
@ -104,7 +106,7 @@ export const DEFAULT_PROJECT_ENVS = [
type TProjectServiceFactoryDep = {
projectDAL: TProjectDALFactory;
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "create">;
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "transaction" | "create" | "findOne" | "updateById">;
projectQueue: TProjectQueueFactory;
userDAL: TUserDALFactory;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
@ -129,7 +131,7 @@ type TProjectServiceFactoryDep = {
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "find" | "create" | "transaction">;
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "find" | "findOne" | "create" | "transaction">;
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create">;
sshCertificateDAL: Pick<TSshCertificateDALFactory, "find" | "countSshCertificatesInProject">;
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find">;
@ -1327,6 +1329,129 @@ export const projectServiceFactory = ({
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 ({
actorId,
actor,
@ -1548,6 +1673,8 @@ export const projectServiceFactory = ({
getProjectKmsBackup,
loadProjectKmsBackup,
getProjectKmsKeys,
getProjectSshConfig,
updateProjectSshConfig,
getProjectSlackConfig,
updateProjectSlackConfig,
requestProjectAccess,

View File

@ -159,6 +159,13 @@ export type TListProjectSshCertificatesDTO = {
limit: number;
} & TProjectPermission;
export type TUpdateProjectSshConfig = {
defaultUserSshCaId?: string;
defaultHostSshCaId?: string;
} & TProjectPermission;
export type TGetProjectSshConfig = TProjectPermission;
export type TGetProjectSlackConfig = TProjectPermission;
export type TUpdateProjectSlackConfig = {

View File

@ -207,7 +207,10 @@ export const fnSecretsV2FromImports = async ({
);
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 importedSecrets = await secretDAL.find(

View File

@ -10,7 +10,8 @@ export enum SecretSync {
TerraformCloud = "terraform-cloud",
Camunda = "camunda",
Vercel = "vercel",
Windmill = "windmill"
Windmill = "windmill",
TeamCity = "teamcity"
}
export enum SecretSyncInitialSyncBehavior {

View File

@ -27,6 +27,7 @@ import { GCP_SYNC_LIST_OPTION } from "./gcp";
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
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 { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
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.Camunda]: CAMUNDA_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 = () => {
@ -140,6 +142,8 @@ export const SecretSyncFns = {
return VercelSyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.Windmill:
return WindmillSyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.TeamCity:
return TeamCitySyncFns.syncSecrets(secretSync, secretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@ -199,6 +203,9 @@ export const SecretSyncFns = {
case SecretSync.Windmill:
secretMap = await WindmillSyncFns.getSecrets(secretSync);
break;
case SecretSync.TeamCity:
secretMap = await TeamCitySyncFns.getSecrets(secretSync);
break;
default:
throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@ -252,6 +259,8 @@ export const SecretSyncFns = {
return VercelSyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.Windmill:
return WindmillSyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.TeamCity:
return TeamCitySyncFns.removeSecrets(secretSync, secretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`

View File

@ -13,7 +13,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.TerraformCloud]: "Terraform Cloud",
[SecretSync.Camunda]: "Camunda",
[SecretSync.Vercel]: "Vercel",
[SecretSync.Windmill]: "Windmill"
[SecretSync.Windmill]: "Windmill",
[SecretSync.TeamCity]: "TeamCity"
};
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.Camunda]: AppConnection.Camunda,
[SecretSync.Vercel]: AppConnection.Vercel,
[SecretSync.Windmill]: AppConnection.Windmill
[SecretSync.Windmill]: AppConnection.Windmill,
[SecretSync.TeamCity]: AppConnection.TeamCity
};

View File

@ -356,8 +356,11 @@ export const secretSyncQueueFactory = ({
};
if (Object.hasOwn(secretMap, key)) {
secretsToUpdate.push(secret);
if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination) importedSecretMap[key] = secretData;
// Only update secrets if the source value is not empty
if (value) {
secretsToUpdate.push(secret);
if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination) importedSecretMap[key] = secretData;
}
} else {
secretsToCreate.push(secret);
importedSecretMap[key] = secretData;

View File

@ -61,6 +61,12 @@ import {
THumanitecSyncListItem,
THumanitecSyncWithCredentials
} from "./humanitec";
import {
TTeamCitySync,
TTeamCitySyncInput,
TTeamCitySyncListItem,
TTeamCitySyncWithCredentials
} from "./teamcity/teamcity-sync-types";
import {
TTerraformCloudSync,
TTerraformCloudSyncInput,
@ -81,7 +87,8 @@ export type TSecretSync =
| TTerraformCloudSync
| TCamundaSync
| TVercelSync
| TWindmillSync;
| TWindmillSync
| TTeamCitySync;
export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials
@ -95,7 +102,8 @@ export type TSecretSyncWithCredentials =
| TTerraformCloudSyncWithCredentials
| TCamundaSyncWithCredentials
| TVercelSyncWithCredentials
| TWindmillSyncWithCredentials;
| TWindmillSyncWithCredentials
| TTeamCitySyncWithCredentials;
export type TSecretSyncInput =
| TAwsParameterStoreSyncInput
@ -109,7 +117,8 @@ export type TSecretSyncInput =
| TTerraformCloudSyncInput
| TCamundaSyncInput
| TVercelSyncInput
| TWindmillSyncInput;
| TWindmillSyncInput
| TTeamCitySyncInput;
export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem
@ -123,7 +132,8 @@ export type TSecretSyncListItem =
| TTerraformCloudSyncListItem
| TCamundaSyncListItem
| TVercelSyncListItem
| TWindmillSyncListItem;
| TWindmillSyncListItem
| TTeamCitySyncListItem;
export type TSyncOptionsConfig = {
canImportSecrets: boolean;

View File

@ -0,0 +1,4 @@
export * from "./teamcity-sync-constants";
export * from "./teamcity-sync-fns";
export * from "./teamcity-sync-schemas";
export * from "./teamcity-sync-types";

View File

@ -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
};

View File

@ -0,0 +1,183 @@
import { request } from "@app/lib/config/request";
import { getTeamCityInstanceUrl } from "@app/services/app-connection/teamcity";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import {
TDeleteTeamCityVariable,
TPostTeamCityVariable,
TTeamCityListVariables,
TTeamCityListVariablesResponse,
TTeamCitySyncWithCredentials
} from "@app/services/secret-sync/teamcity/teamcity-sync-types";
// Note: Most variables won't be returned with a value due to them being a "password" type (starting with "env.").
// TeamCity API returns empty string for password-type variables for security reasons.
const listTeamCityVariables = async ({ instanceUrl, accessToken, project, buildConfig }: TTeamCityListVariables) => {
const { data } = await request.get<TTeamCityListVariablesResponse>(
buildConfig
? `${instanceUrl}/app/rest/buildTypes/${encodeURIComponent(buildConfig)}/parameters`
: `${instanceUrl}/app/rest/projects/id:${encodeURIComponent(project)}/parameters`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
// Strips out "env." from map key, but the "name" field still has the original unaltered key.
return Object.fromEntries(
data.property.map((variable) => [
variable.name.startsWith("env.") ? variable.name.substring(4) : variable.name,
{ ...variable, value: variable.value || "" } // Password values will be empty strings from the API for security
])
);
};
// Create and update both use the same method
const updateTeamCityVariable = async ({
instanceUrl,
accessToken,
project,
buildConfig,
key,
value
}: TPostTeamCityVariable) => {
return request.post(
buildConfig
? `${instanceUrl}/app/rest/buildTypes/${encodeURIComponent(buildConfig)}/parameters`
: `${instanceUrl}/app/rest/projects/id:${encodeURIComponent(project)}/parameters`,
{
name: key,
value,
type: {
rawValue: "password display='hidden'"
}
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
);
};
const deleteTeamCityVariable = async ({
instanceUrl,
accessToken,
project,
buildConfig,
key
}: TDeleteTeamCityVariable) => {
return request.delete(
buildConfig
? `${instanceUrl}/app/rest/buildTypes/${encodeURIComponent(buildConfig)}/parameters/${encodeURIComponent(key)}`
: `${instanceUrl}/app/rest/projects/id:${encodeURIComponent(project)}/parameters/${encodeURIComponent(key)}`,
{
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
};
export const TeamCitySyncFns = {
syncSecrets: async (secretSync: TTeamCitySyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
destinationConfig: { project, buildConfig }
} = secretSync;
const instanceUrl = await getTeamCityInstanceUrl(connection);
const { accessToken } = connection.credentials;
for await (const entry of Object.entries(secretMap)) {
const [key, { value }] = entry;
const payload = {
instanceUrl,
accessToken,
project,
buildConfig,
key: `env.${key}`,
value
};
try {
// Replace every secret since TeamCity does not return secret values that we can cross-check
// No need to differenciate create / update because TeamCity uses the same method for both
await updateTeamCityVariable(payload);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
if (secretSync.syncOptions.disableSecretDeletion) return;
const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig });
for await (const [key, variable] of Object.entries(variables)) {
if (!(key in secretMap)) {
try {
await deleteTeamCityVariable({
key: variable.name, // We use variable.name instead of key because key is stripped of "env." prefix in listTeamCityVariables().
instanceUrl,
accessToken,
project,
buildConfig
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
}
},
removeSecrets: async (secretSync: TTeamCitySyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
destinationConfig: { project, buildConfig }
} = secretSync;
const instanceUrl = await getTeamCityInstanceUrl(connection);
const { accessToken } = connection.credentials;
const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig });
for await (const [key, variable] of Object.entries(variables)) {
if (key in secretMap) {
try {
await deleteTeamCityVariable({
key: variable.name, // We use variable.name instead of key because key is stripped of "env." prefix in listTeamCityVariables().
instanceUrl,
accessToken,
project,
buildConfig
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
}
},
getSecrets: async (secretSync: TTeamCitySyncWithCredentials) => {
const {
connection,
destinationConfig: { project, buildConfig }
} = secretSync;
const instanceUrl = await getTeamCityInstanceUrl(connection);
const { accessToken } = connection.credentials;
return listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig });
}
};

View File

@ -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)
});

View File

@ -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;
};

View File

@ -44,6 +44,7 @@ export enum SmtpTemplates {
SecretRotationFailed = "secretRotationFailed.handlebars",
ProjectAccessRequest = "projectAccess.handlebars",
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars",
OrgAdminBreakglassAccess = "orgAdminBreakglassAccess.handlebars",
ServiceTokenExpired = "serviceTokenExpired.handlebars"
}

View File

@ -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>

View File

@ -177,7 +177,6 @@ func issueCredentials(cmd *cobra.Command, args []string) {
infisicalToken = token.Token
} else {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
@ -411,7 +410,6 @@ func signKey(cmd *cobra.Command, args []string) {
infisicalToken = token.Token
} else {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
@ -610,25 +608,82 @@ func signKey(cmd *cobra.Command, args []string) {
}
func sshConnect(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
token, err := util.GetInfisicalToken(cmd)
if err != nil {
util.HandleError(err, "Unable to authenticate")
util.HandleError(err, "Unable to parse flag")
}
var infisicalToken string
if loggedInUserDetails.LoginExpired {
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
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")
if err != nil {
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()
if err != nil {
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")
}
// Prompt to select host
hostNames := make([]string, len(hosts))
for i, h := range hosts {
hostNames[i] = h.Hostname
var selectedHost = hosts[0]
if hostname != "" {
foundHost := false
for _, h := range hosts {
if h.Hostname == hostname {
selectedHost = h
foundHost = true
break
}
}
if !foundHost {
util.PrintErrorMessageAndExit("Specified --hostname not found or not accessible")
}
} else {
hostNames := make([]string, len(hosts))
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{
Label: "Select an SSH Host",
Items: hostNames,
Size: 10,
var selectedLoginUser string
if loginUser != "" {
foundLoginUser := false
for _, m := range selectedHost.LoginMappings {
if m.LoginUser == loginUser {
selectedLoginUser = loginUser
foundLoginUser = true
break
}
}
if !foundLoginUser {
util.PrintErrorMessageAndExit("Specified --loginUser not valid for selected host")
}
} else {
if len(selectedHost.LoginMappings) == 0 {
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
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")
}
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
err = addCredentialsToAgent(creds.PrivateKey, creds.SignedKey)
if err != nil {
@ -769,7 +866,6 @@ func sshAddHost(cmd *cobra.Command, args []string) {
infisicalToken = token.Token
} else {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
@ -1006,16 +1102,20 @@ func init() {
sshIssueCredentialsCmd.Flags().Bool("addToAgent", false, "Whether to add issued SSH credentials to the SSH agent")
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)
sshAddHostCmd.Flags().String("token", "", "Use a machine identity access token")
sshAddHostCmd.Flags().String("projectId", "", "Project ID the host belongs to (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().String("userCaOutFilePath", "/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("configureSshd", false, "Update TrustedUserCAKeys, HostKey, and HostCertificate in the sshd_config file")
sshAddHostCmd.Flags().Bool("write-user-ca-to-file", false, "Write User CA public key to /etc/ssh/infisical_user_ca.pub")
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("write-host-cert-to-file", false, "Write SSH host certificate to /etc/ssh/ssh_host_<type>_key-cert.pub")
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")
sshCmd.AddCommand(sshAddHostCmd)

View File

@ -1,9 +1,12 @@
package cmd
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"github.com/Infisical/infisical-merge/packages/config"
"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{
Use: "update",
Short: "Used to update properties of an Infisical profile",
@ -185,6 +239,8 @@ var domainCmd = &cobra.Command{
func init() {
updateCmd.AddCommand(domainCmd)
userCmd.AddCommand(updateCmd)
userGetCmd.AddCommand(userGetTokenCmd)
userCmd.AddCommand(userGetCmd)
userCmd.AddCommand(switchCmd)
rootCmd.AddCommand(userCmd)
}

View File

@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/ldap/available"
---

View File

@ -0,0 +1,9 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/ldap"
---
<Note>
Check out the configuration docs for [LDAP Connections](/integrations/app-connections/ldap) to learn how to obtain
the required credentials.
</Note>

View File

@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/ldap/{connectionId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/ldap/{connectionId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/ldap/connection-name/{connectionName}"
---

View File

@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/ldap"
---

View File

@ -0,0 +1,9 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/ldap/{connectionId}"
---
<Note>
Check out the configuration docs for [LDAP Connections](/integrations/app-connections/ldap) to learn how to obtain
the required credentials.
</Note>

View File

@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/teamcity/available"
---

View File

@ -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>

View File

@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/teamcity/{connectionId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/teamcity/{connectionId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/teamcity/connection-name/{connectionName}"
---

View File

@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/teamcity"
---

View File

@ -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>

View File

@ -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>

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