Compare commits

...

67 Commits

Author SHA1 Message Date
Scott Wilson
fa8154ecdd improvement: add undefined handling 2025-04-25 15:06:16 -07:00
Scott Wilson
d977092502 improvement: improve validate aws connection error propagation 2025-04-25 15:05:22 -07:00
Andrey
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
carlosmonastyrski
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
carlosmonastyrski
b506393765 feat(aws-iam-rotation): docs improvements 2025-04-25 16:35:57 -03:00
carlosmonastyrski
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
BlackMagiq
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
Andrey
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
Tuan Dang
0c214a2f26 Adjust CLI flags to be dash-case 2025-04-25 10:03:51 -07:00
Tuan Dang
f5862cbb9a Merge 2025-04-25 09:32:48 -07:00
Tuan Dang
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
Sheen
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
carlosmonastyrski
7a4a877e39 feat(aws-iam-rotation): remove credentials validation due to excesive await time 2025-04-25 12:38:41 -03:00
carlosmonastyrski
8f670bde88 feat(aws-iam-rotation): add credentials validation 2025-04-25 12:06:30 -03:00
carlosmonastyrski
ff9011c899 feat(aws-iam-rotation): add view credentials component 2025-04-25 11:23:43 -03:00
carlosmonastyrski
57c96abe03 feat(aws-iam-rotation): address PR comments 2025-04-25 11:01:35 -03:00
Sheen Capadngan
178acc412d misc: added optional accesS 2025-04-25 20:52:55 +08:00
Sheen Capadngan
b0288c49c0 feat: add user get token CLI and revamp session management 2025-04-25 20:43:20 +08:00
carlosmonastyrski
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
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
Maidul Islam
c613bb642e Merge pull request #3485 from Infisical/daniel/kms-logs
fix(kms): better error logs
2025-04-24 17:06:01 -07:00
Daniel Hougaard
90fdba0b77 Update kms-service.ts 2025-04-25 04:04:26 +04:00
Daniel Hougaard
795ce11062 Update kms-service.ts 2025-04-25 04:00:14 +04:00
Daniel Hougaard
2d4adfc651 fix(kms): better error logs 2025-04-25 03:54:59 +04:00
carlosmonastyrski
cb826f1a77 fix(dynamic-secret): improve error propagation and add FAQ to docs 2025-04-24 19:21:30 -03:00
Maidul Islam
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
Maidul Islam
a19e5ff905 add min version 2025-04-24 14:16:56 -07:00
Maidul Islam
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
Maidul Islam
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
Maidul Islam
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
Maidul Islam
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
Maidul Islam
aff318cf3c Merge branch 'main' into doc/infisical-package 2025-04-24 14:12:01 -07:00
Maidul Islam
c97a3f07a7 update linux docs 2025-04-24 14:10:21 -07:00
carlosmonastyrski
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
carlosmonastyrski
4973447676 feat(project-members): PR suggestions improvements 2025-04-24 12:21:19 -03:00
carlosmonastyrski
bd2e2b7931 feat(project-members): PR suggestions improvements 2025-04-24 12:14:06 -03:00
Andrey
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
Akhil Mohan
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
carlosmonastyrski
aa893a40a9 feat(project-members): Persist pagination setting and add role filtering 2025-04-24 10:06:09 -03: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
carlosmonastyrski
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
carlosmonastyrski
b85809293c Lint fix 2025-04-22 13:53:56 -03:00
carlosmonastyrski
f143d8c358 Merge branch 'main' into feat/awsSecretRotationV2 2025-04-22 13:46:35 -03:00
carlosmonastyrski
2e3330bf69 Add AWS secret rotation V2 2025-04-22 13:26:48 -03:00
Tuan Dang
1ea8e5a81e Add frontend uniqueness check for ssh hostnames 2025-04-18 15:25:13 -07:00
Tuan Dang
42aa3c3d46 Remove extra tx in ssh nullable ca defaults migration, update ssh docs 2025-04-18 11:06:59 -07:00
Tuan Dang
184d353de5 Update infisical ssh docs to clarify ssh connect command in different modes 2025-04-17 23:29:20 -07:00
Tuan Dang
b2360f9cc8 Reuse writeToFile fn in ssh connect command 2025-04-17 23:12:44 -07:00
Tuan Dang
846a5a6e19 impl improvements according to greptile 2025-04-17 23:08:33 -07:00
Tuan Dang
c6cd3a8cc0 Add audit logs to project ssh config endpoints 2025-04-17 23:00:46 -07:00
Tuan Dang
796f5510ca Add cli docs for infisical ssh connect command 2025-04-17 22:40:43 -07:00
Tuan Dang
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
196 changed files with 4269 additions and 516 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,7 @@
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 { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
@@ -12,5 +13,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
> = {
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter
};

View File

@@ -2,6 +2,7 @@ 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 { 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 +14,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
PostgresCredentialsRotationListItemSchema,
MsSqlCredentialsRotationListItemSchema,
Auth0ClientSecretRotationListItemSchema
Auth0ClientSecretRotationListItemSchema,
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

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

View File

@@ -4,6 +4,7 @@ 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 { 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 +19,8 @@ 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.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION
};
export const listSecretRotationOptions = () => {

View File

@@ -3,12 +3,14 @@ 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.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.AwsIamUserSecret]: AppConnection.AWS
};

View File

@@ -77,6 +77,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 +115,8 @@ 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.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation
};
export const secretRotationV2ServiceFactory = ({

View File

@@ -12,6 +12,13 @@ import {
TAuth0ClientSecretRotationListItem,
TAuth0ClientSecretRotationWithConnection
} from "./auth0-client-secret";
import {
TAwsIamUserSecretRotation,
TAwsIamUserSecretRotationGeneratedCredentials,
TAwsIamUserSecretRotationInput,
TAwsIamUserSecretRotationListItem,
TAwsIamUserSecretRotationWithConnection
} from "./aws-iam-user-secret";
import {
TMsSqlCredentialsRotation,
TMsSqlCredentialsRotationInput,
@@ -27,26 +34,34 @@ 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
| TAwsIamUserSecretRotation;
export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection
| TMsSqlCredentialsRotationWithConnection
| TAuth0ClientSecretRotationWithConnection;
| TAuth0ClientSecretRotationWithConnection
| TAwsIamUserSecretRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials
| TAuth0ClientSecretRotationGeneratedCredentials;
| TAuth0ClientSecretRotationGeneratedCredentials
| TAwsIamUserSecretRotationGeneratedCredentials;
export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput
| TMsSqlCredentialsRotationInput
| TAuth0ClientSecretRotationInput;
| TAuth0ClientSecretRotationInput
| TAwsIamUserSecretRotationInput;
export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem
| TMsSqlCredentialsRotationListItem
| TAuth0ClientSecretRotationListItem;
| TAuth0ClientSecretRotationListItem
| TAwsIamUserSecretRotationListItem;
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;

View File

@@ -4,8 +4,11 @@ import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotatio
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,
AwsIamUserSecretRotationSchema
]);

View File

@@ -1857,6 +1857,10 @@ 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."
},
TEAMCITY: {
instanceUrl: "The TeamCity instance URL to connect with.",
accessToken: "The access token to use to connect with TeamCity."
}
}
};
@@ -1996,6 +2000,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 +2068,10 @@ export const SecretRotations = {
},
AUTH0_CLIENT_SECRET: {
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
},
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 +2082,10 @@ 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."
},
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

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

@@ -33,6 +33,10 @@ import {
PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema
} from "@app/services/app-connection/postgres";
import {
SanitizedTeamCityConnectionSchema,
TeamCityConnectionListItemSchema
} from "@app/services/app-connection/teamcity";
import {
SanitizedTerraformCloudConnectionSchema,
TerraformCloudConnectionListItemSchema
@@ -59,7 +63,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedMsSqlConnectionSchema.options,
...SanitizedCamundaConnectionSchema.options,
...SanitizedWindmillConnectionSchema.options,
...SanitizedAuth0ConnectionSchema.options
...SanitizedAuth0ConnectionSchema.options,
...SanitizedTeamCityConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -76,7 +81,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
MsSqlConnectionListItemSchema,
CamundaConnectionListItemSchema,
WindmillConnectionListItemSchema,
Auth0ConnectionListItemSchema
Auth0ConnectionListItemSchema,
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

@@ -11,6 +11,7 @@ import { registerGitHubConnectionRouter } from "./github-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-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 +33,6 @@ 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.TeamCity]: registerTeamCityConnectionRouter
};

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,8 @@ export enum AppConnection {
MsSql = "mssql",
Camunda = "camunda",
Windmill = "windmill",
Auth0 = "auth0"
Auth0 = "auth0",
TeamCity = "teamcity"
}
export enum AWSRegion {

View File

@@ -43,6 +43,11 @@ import {
} from "./humanitec";
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import {
getTeamCityConnectionListItem,
TeamCityConnectionMethod,
validateTeamCityConnectionCredentials
} from "./teamcity";
import {
getTerraformCloudConnectionListItem,
TerraformCloudConnectionMethod,
@@ -71,7 +76,8 @@ export const listAppConnectionOptions = () => {
getMsSqlConnectionListItem(),
getCamundaConnectionListItem(),
getWindmillConnectionListItem(),
getAuth0ConnectionListItem()
getAuth0ConnectionListItem(),
getTeamCityConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -135,7 +141,8 @@ 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.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@@ -167,6 +174,7 @@ 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";
@@ -214,5 +222,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
};

View File

@@ -14,5 +14,6 @@ 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.TeamCity]: "TeamCity"
};

View File

@@ -45,6 +45,8 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
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 +76,8 @@ 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.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -450,6 +453,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

@@ -63,6 +63,12 @@ import {
TPostgresConnectionInput,
TValidatePostgresConnectionCredentialsSchema
} from "./postgres";
import {
TTeamCityConnection,
TTeamCityConnectionConfig,
TTeamCityConnectionInput,
TValidateTeamCityConnectionCredentialsSchema
} from "./teamcity";
import {
TTerraformCloudConnection,
TTerraformCloudConnectionConfig,
@@ -97,6 +103,7 @@ export type TAppConnection = { id: string } & (
| TCamundaConnection
| TWindmillConnection
| TAuth0Connection
| TTeamCityConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -118,6 +125,7 @@ export type TAppConnectionInput = { id: string } & (
| TCamundaConnectionInput
| TWindmillConnectionInput
| TAuth0ConnectionInput
| TTeamCityConnectionInput
);
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
@@ -144,7 +152,8 @@ export type TAppConnectionConfig =
| TSqlConnectionConfig
| TCamundaConnectionConfig
| TWindmillConnectionConfig
| TAuth0ConnectionConfig;
| TAuth0ConnectionConfig
| TTeamCityConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -160,7 +169,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateTerraformCloudConnectionCredentialsSchema
| TValidateVercelConnectionCredentialsSchema
| TValidateWindmillConnectionCredentialsSchema
| TValidateAuth0ConnectionCredentialsSchema;
| TValidateAuth0ConnectionCredentialsSchema
| TValidateTeamCityConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
@@ -168,6 +178,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 "./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";
@@ -11,6 +13,7 @@ import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
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 +31,15 @@ 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";
import { removeTrailingSlash } from "@app/lib/fn";
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

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

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret/rotation-name/{rotationName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get Credentials by ID"
openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}/generated-credentials"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret"
---

View File

@@ -0,0 +1,4 @@
---
title: "Rotate Secrets"
openapi: "POST /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}/rotate-secrets"
---

View File

@@ -0,0 +1,9 @@
---
title: "Update"
openapi: "PATCH /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}"
---
<Note>
Check out the configuration docs for [AWS IAM User Secret Rotations](/documentation/platform/secret-rotation/aws-iam-user-secret) to learn how to obtain the
required parameters.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/teamcity"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/teamcity/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/teamcity/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/teamcity/sync-name/{syncName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Import Secrets"
openapi: "POST /api/v1/secret-syncs/teamcity/{syncId}/import-secrets"
---

View File

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

View File

@@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/teamcity/{syncId}/remove-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/teamcity/{syncId}/sync-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/secret-syncs/teamcity/{syncId}"
---

View File

@@ -7,10 +7,38 @@ description: "Generate SSH credentials with the CLI"
[Infisical SSH](/documentation/platform/ssh) lets you issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure.
This command enables you to obtain SSH credentials used to access a remote host; we recommend using the `issue-credentials` sub-command to generate dynamic SSH credentials for each SSH session.
This command enables you to obtain SSH credentials used to access a remote host. We recommend using the `connect` sub-command which handles the full workflow of issuing credentials and establishing an SSH connection in one step.
### Sub-commands
<Accordion title="infisical ssh connect">
This command is used to connect to an SSH host using issued credentials. It will automatically issue credentials and either add them to your SSH agent or write them to disk before establishing an SSH connection.
```bash
$ infisical ssh connect
```
### Flags
<Accordion title="--hostname">
The hostname of the SSH host to connect to. If not provided, you will be prompted to select from available hosts.
</Accordion>
<Accordion title="--loginUser">
The login user for the SSH connection. If not provided, you will be prompted to select from available login users.
</Accordion>
<Accordion title="--writeHostCaToFile">
Whether to write the Host CA public key to `~/.ssh/known_hosts` if it doesn't already exist.
Default value: `true`
</Accordion>
<Accordion title="--outFilePath">
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.
</Accordion>
<Accordion title="--token">
An authenticated token to use to authenticate with Infisical.
</Accordion>
</Accordion>
<Accordion title="infisical ssh issue-credentials">
This command is used to issue SSH credentials (SSH certificate, public key, and private key) against a certificate template.
@@ -29,43 +57,44 @@ This command enables you to obtain SSH credentials used to access a remote host;
</Accordion>
<Accordion title="--addToAgent">
Whether to add issued SSH credentials to the SSH agent.
Default value: `false`
Note that either the `--outFilePath` or `--addToAgent` flag must be set for the sub-command to execute successfully.
</Accordion>
<Accordion title="--outFilePath">
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 saved to the current working directory where the command is run.
Note that either the `--outFilePath` or `--addToAgent` flag must be set for the sub-command to execute successfully.
</Accordion>
<Accordion title="--keyAlgorithm">
The key algorithm to issue SSH credentials for.
Default value: `RSA_2048`
Available options: `RSA_2048`, `RSA_4096`, `EC_prime256v1`, `EC_secp384r1`.
</Accordion>
<Accordion title="--certType">
The certificate type to issue SSH credentials for.
Default value: `user`
Available options: `user` or `host`
</Accordion>
<Accordion title="--ttl">
The time-to-live (TTL) for the issued SSH certificate (e.g. `2 days`, `1d`, `2h`, `1y`).
Defaults to the Default TTL value set in the certificate template.
</Accordion>
<Accordion title="--keyId">
A custom Key ID to issue SSH credentials for.
Defaults to the autogenerated Key ID by Infisical.
</Accordion>
<Accordion title="--token">
An authenticated token to use to issue SSH credentials.
</Accordion>
</Accordion>
<Accordion title="infisical ssh sign-key">
@@ -95,22 +124,23 @@ This command enables you to obtain SSH credentials used to access a remote host;
</Accordion>
<Accordion title="--certType">
The certificate type to issue SSH credentials for.
Default value: `user`
Available options: `user` or `host`
</Accordion>
<Accordion title="--ttl">
The time-to-live (TTL) for the issued SSH certificate (e.g. `2 days`, `1d`, `2h`, `1y`).
Defaults to the Default TTL value set in the certificate template.
</Accordion>
<Accordion title="--keyId">
A custom Key ID to issue SSH credentials for.
Defaults to the autogenerated Key ID by Infisical.
</Accordion>
<Accordion title="--token">
An authenticated token to use to issue SSH credentials.
</Accordion>
</Accordion>
</Accordion>

View File

@@ -8,22 +8,46 @@ infisical user
```
## Description
This command allows you to manage the current logged in users on the CLI
### Sub-commands
<Accordion title="infisical user switch" defaultOpen="true">
Use this command to switch between profiles that are currently logged into the CLI
### Sub-commands
<Accordion title="infisical user switch" defaultOpen="true">
Use this command to switch between profiles that are currently logged into the CLI
```bash
infisical user switch
```
```bash
infisical user switch
```
</Accordion>
<Accordion title="infisical user update domain">
With this command, you can modify the backend API that is utilized for all requests associated with a specific profile.
For instance, you have the option to point the profile to use either the Infisical Cloud or your own self-hosted Infisical instance.
```bash
infisical user update domain
```bash
infisical user update domain
```
</Accordion>
<Accordion title="infisical user get token">
Use this command to get your current Infisical access token and session information. This command requires you to be logged in.
The command will display:
- Your session ID
- Your full JWT access token
```bash
infisical user get token
```
Example output:
```bash
Session ID: abc123-xyz-456
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
</Accordion>

View File

@@ -41,3 +41,16 @@ Dynamic secrets are particularly useful in environments with stringent security
4. [Oracle](./oracle)
6. [Redis](./redis)
5. [AWS IAM](./aws-iam)
**FAQ**
<AccordionGroup>
<Accordion title="Why is my SQL dynamic secret failing when I generate a lease?">
This usually happens when the SQL statements defined for creating or revoking the secret are not compatible with your database provider.
Different SQL engines have different expectations for quoting identifiers and values. For example, some use backticks (`` `username` ``), others use single quotes (`'username'`), and some expect double quotes (`"username"`). A statement that works on one provider might fail on another.
**Recommendation:**
Make sure to adjust your SQL statements to follow the syntax required by your specific database provider. Always test them directly on your target database to ensure they execute without errors.
</Accordion>
</AccordionGroup>

View File

@@ -19,7 +19,7 @@ Before you begin, you'll first need to choose a method of authentication with AW
![IAM Role Creation](/images/integrations/aws/integration-aws-iam-assume-role.png)
2. Select **AWS Account** as the **Trusted Entity Type**.
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted.
3. Select **Another AWS Account** and provide the appropriate Infisical AWS Account ID: use **381492033652** for the **US region**, and **345594589636** for the **EU region**. This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted.
4. Optionally, enable **Require external ID** and enter your Infisical **project ID** to further enhance security.
</Step>
<Step title="Add Required Permissions for the IAM Role">

View File

@@ -0,0 +1,191 @@
---
title: "AWS IAM User"
description: "Learn how to automatically rotate Access Key Id and Secret Key of AWS IAM Users."
---
Infisical's AWS IAM User secret rotation capability lets you update the **Access key** and **Secret access key** credentials of a target IAM user from within Infisical
at a specified interval or on-demand.
## Prerequisites
- Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Rotation** permissions
- Make sure to add the following permissions to your IAM Role/IAM User Permission policy set used by your AWS Connection:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:ListAccessKeys",
"iam:CreateAccessKey",
"iam:UpdateAccessKey",
"iam:DeleteAccessKey",
"iam:ListUsers"
],
"Resource": "*"
}
]
}
```
## Workflow
The typical workflow for using the AWS IAM User rotation strategy consists of four steps:
1. Creating the target IAM user whose credentials you wish to rotate.
2. Configuring the rotation strategy in Infisical with the credentials of the managing IAM user.
3. Pressing the **Rotate** button in the Infisical dashboard to trigger the rotation of the target IAM user's credentials. The strategy can also be configured to rotate the credentials automatically at a specified interval.
In the following steps, we explore the end-to-end workflow for setting up this strategy in Infisical.
<Steps>
<Step title="Create the target IAM user">
To begin, create an IAM user whose credentials you wish to rotate. If you already have an IAM user,
then you can skip this step.
</Step>
<Step title="Configure the AWS IAM User secret rotation strategy in Infisical">
<Tabs>
<Tab title="Infisical UI">
1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown.
![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png)
2. Select the **AWS IAM User Secret** option.
![Select AWS IAM User Secret](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-option.png)
3. Select the **AWS Connection** to use and configure the rotation behavior. Then click **Next**.
![Rotation Configuration](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-configuration.png)
- **AWS Connection** - the connection that will perform the rotation of the specified application's Client Secret.
- **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation.
- **Rotate At** - the local time of day when rotation should occur once the interval has elapsed.
- **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation.
4. Select the AWS IAM user and the region of the user whose credentials you want to rotate. Then click **Next**.
![Rotation Parameters](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-parameters.png)
5. Specify the secret names that the AWS IAM access key credentials should be mapped to. Then click **Next**.
![Rotation Secrets Mapping](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-secrets-mapping.png)
- **Access Key ID** - the name of the secret that the AWS access key ID will be mapped to.
- **Secret Access Key** - the name of the secret that the rotated secret access key will be mapped to.
6. Give your rotation a name and description (optional). Then click **Next**.
![Rotation Details](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-details.png)
- **Name** - the name of the secret rotation configuration. Must be slug-friendly.
- **Description** (optional) - a description of this rotation configuration.
7. Review your configuration, then click **Create Secret Rotation**.
![Rotation Review](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-confirm.png)
8. Your **AWS IAM User** credentials are now available for use via the mapped secrets.
![Rotation Created](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-created.png)
</Tab>
<Tab title="API">
To create an AWS IAM User Rotation, make an API request to the [Create AWS IAM User Rotation](/api-reference/endpoints/secret-rotations/aws-iam-user-secret/create) API endpoint.
You will first need the **User Name** of the AWS IAM user you want to rotate the secret for. This can be obtained from the IAM console, on Users tab.
![Users](/images/secret-rotations-v2/aws-iam-user-secret/aws-iam-user-secret-user-names.png)
### Sample request
```bash Request
curl --request POST \
--url https://us.infisical.com/api/v2/secret-rotations/aws-iam-user-secret \
--header 'Content-Type: application/json' \
--data '{
"name": "my-aws-rotation",
"projectId": "9602cfc5-20b9-4c35-a056-dd7372db0f25",
"description": "My rotation strategy description",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"environment": "dev",
"secretPath": "/",
"isAutoRotationEnabled": true,
"rotationInterval": 2,
"rotateAtUtc": {
"hours": 11.5,
"minutes": 29.5
},
"parameters": {
"userName": "testUser",
"region": "us-east-1"
},
"secretsMapping": {
"accessKeyId": "AWS_ACCESS_KEY_ID",
"secretAccessKey": "AWS_SECRET_ACCESS_KEY"
}
}'
```
### Sample response
```bash Response
{
"secretRotation": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-aws-rotation",
"description": "My rotation strategy description",
"secretsMapping": {
"accessKeyId": "AWS_ACCESS_KEY_ID",
"secretAccessKey": "AWS_SECRET_ACCESS_KEY"
},
"isAutoRotationEnabled": true,
"activeIndex": 0,
"folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
"rotationInterval": 123,
"rotationStatus": "success",
"lastRotationAttemptedAt": "2023-11-07T05:31:56Z",
"lastRotatedAt": "2023-11-07T05:31:56Z",
"lastRotationJobId": null,
"nextRotationAt": "2023-11-07T05:31:56Z",
"isLastRotationManual": true,
"connection": {
"app": "aws",
"name": "my-aws-connection",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"environment": {
"slug": "dev",
"name": "Development",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"projectId": "9602cfc5-20b9-4c35-a056-dd7372db0f25",
"folder": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"path": "/"
},
"rotateAtUtc": {
"hours": 11.5,
"minutes": 29.5
},
"lastRotationMessage": null,
"type": "aws-iam-user-secret",
"parameters": {
"userName": "testUser",
"region": "us-east-1"
}
}
}
```
</Tab>
</Tabs>
</Step>
</Steps>
**FAQ**
<AccordionGroup>
<Accordion title="Why are my AWS IAM credentials not rotating?">
There are a few reasons for why this might happen:
- The strategy configuration is invalid (e.g. the managing IAM user's credentials are incorrect, the target AWS region is incorrect, etc.)
- The managing IAM user is insufficently permissioned to rotate the credentials of the target IAM user. For instance, you may have setup
[paths](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) for the managing IAM user and the policy does not have the necessary
permissions to rotate the credentials.
</Accordion>
</AccordionGroup>

View File

@@ -1,143 +0,0 @@
---
title: "AWS IAM User"
description: "Learn how to automatically rotate Access Key Id and Secret Key of AWS IAM Users."
---
Infisical's AWS IAM User secret rotation capability lets you update the **Access key** and **Secret access key** credentials of a target IAM user from within Infisical
at a specified interval or on-demand.
## Workflow
The typical workflow for using the AWS IAM User rotation strategy consists of four steps:
1. Creating the target IAM user whose credentials you wish to rotate.
2. Creating the managing IAM user used by Infisical to rotate the credentials of the target IAM user.
3. Configuring the rotation strategy in Infisical with the credentials of the managing IAM user.
4. Pressing the **Rotate** button in the Infisical dashboard to trigger the rotation of the target IAM user's credentials. The strategy can also be configured to rotate the credentials automatically at a specified interval.
In the following steps, we explore the end-to-end workflow for setting up this strategy in Infisical.
<Steps>
<Step title="Create the target IAM user">
To begin, create an IAM user whose credentials you wish to rotate. If you already have an IAM user,
then you can skip this step.
</Step>
<Step title="Create the managing IAM user">
Next, create another IAM user to be used by Infisical to rotate the credentials of the IAM user in the previous step.
2.1. In your AWS console, head to IAM > Access management > Users and press **Create user**.
![iam user secret rotation create user](../../../images/platform/secret-rotation/aws-iam/rotation-manager-create-user.png)
2.2. Next, give the user a username like **infisical-rotation-manager** and press **Next**.
![iam user secret rotation username](../../../images/platform/secret-rotation/aws-iam/rotation-manager-username.png)
2.3. Next, in the **Set permissions** step, select **Attach policies directly** and then press **Create policy**.
![iam user secret rotation create policy](../../../images/platform/secret-rotation/aws-iam/rotation-manager-create-policy.png)
2.4. Next, in the **Policy editor**, paste the following JSON and press **Next**:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"iam:DeleteAccessKey",
"iam:GetAccessKeyLastUsed",
"iam:CreateAccessKey"
],
"Resource": "*"
}
]
}
```
<Note>
The IAM policy above uses the wildcard option in Resource: "*".
You may want to restrict the policy to a specific path, and make any adjustments as necessary, to control access for the managing user in production.
Read more about this [here](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/).
</Note>
In the **Review and create** step, give the policy a name like **infisical-rotation-manager**, press **Create policy** to finish creating the policy.
![iam user secret rotation policy review](../../../images/platform/secret-rotation/aws-iam/rotation-manager-policy-review.png)
2.5. Back in the **Set permissions** step from step 2.3, refresh the policy list and search for the policy you just created from step 2.4.
Select the policy and press **Next**.
![iam user secret rotation attach policy](../../../images/platform/secret-rotation/aws-iam/rotation-manager-attach-policy.png)
In the **Review and create** step, press **Create user** to finish creating the IAM user.
![iam user secret rotation manager user review](../../../images/platform/secret-rotation/aws-iam/rotation-manager-user-review.png)
2.5. Having created the user, head to its Security credentials > Access keys and press **Create access key**.
Follow the subsequent steps to create the **access key** and **secret access key** credential pair for the user.
![iam user secret rotation manager create access key](../../../images/platform/secret-rotation/aws-iam/rotation-manager-create-access-key.png)
At the end of the flow, copy the **Access key** and **Secret access key** to use when configuring the AWS IAM User rotation strategy back in Infisical next.
![iam user secret rotation manager access keys](../../../images/platform/secret-rotation/aws-iam/rotation-manager-access-keys.png)
</Step>
<Step title="Configure the AWS IAM User secret rotation strategy in Infisical">
3.1. Back in Infisical, head to the Project > Secrets > Environment and path where you want the rotated AWS IAM credentials to appear and create two placeholder secrets.
In this example, we'll create two secrets called `AWS_ACCESS_KEY` and `AWS_SECRET_ACCESS_KEY`.
![iam user secret rotation secrets](../../../images/platform/secret-rotation/aws-iam/rotation-config-secrets.png)
3.2. Next, in the **Secret Rotation** tab, press on the **AWS IAM** tile to configure the AWS IAM User rotation strategy.
![iam user secret rotation select aws iam user method](../../../images/platform/secret-rotation/aws-iam/rotations-select-aws-iam-user.png)
3.3. Input the configuration details for the AWS IAM User rotation strategy obtained from steps 1 and 2:
![iam user secret rotation config 1](../../../images/platform/secret-rotation/aws-iam/rotation-config-1.png)
Here's some guidance on each field:
- Manager User Access Key: The managing IAM user's access key from step 2.5.
- Manager User Secret Key: The managing IAM user's secret access key from step 2.5.
- Manager User AWS Region: The [AWS region](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html) for Infisical to make requests to such as `us-east-1`.
- IAM Username: The IAM username of the user from step 1.
Next, specify the output secret mappings configuration for the rotated AWS IAM credentials; this is the secrets whose values will be replaced with new credentials after each rotation.
Here, you can also specify a rotation interval for the credentials to be automatically rotated periodically.
In this example, we want to map the output of the rotated AWS IAM credentials to the secrets that we created in step 3.1 (i.e. `AWS_ACCESS_KEY` and `AWS_SECRET_ACCESS_KEY`).
![iam user secret rotation config 2](../../../images/platform/secret-rotation/aws-iam/rotation-config-2.png)
Finally, press **Submit** to create the secret rotation strategy.
</Step>
<Step title="Rotate secrets in Infisical">
You should now see the AWS IAM User rotation strategy listed in the **Secret Rotation** tab.
To manually trigger a rotation, you can press the **Rotate** button on the strategy.
Once triggered, the secrets in step 3.1 should be updated with new rotated credential values.
![iam user secret rotations aws iam user](../../../images/platform/secret-rotation/aws-iam/rotations-aws-iam-user.png)
</Step>
</Steps>
**FAQ**
<AccordionGroup>
<Accordion title="Why are my AWS IAM credentials not rotating?">
There are a few reasons for why this might happen:
- The strategy configuration is invalid (e.g. the managing IAM user's credentials are incorrect, the target IAM username is incorrect, etc.).
- The managing IAM user is insufficently permissioned to rotate the credentials of the target IAM user. For instance, you may have setup [paths](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) for the managing IAM user and the policy does not have the necessary permissions to rotate the credentials.
- The target IAM user already has 2 access keys configured in AWS; you should delete one of the access keys to allow for rotation.
</Accordion>
</AccordionGroup>

View File

@@ -10,10 +10,10 @@ Infisical SSH can be configured to provide users on your team short-lived, secur
and improves upon traditional SSH key-based authentication by mitigating private key compromise, static key management,
unauthorized access, and SSH key sprawl.
The following entities and concepts are important to understand when using Infisical SSH:
The following entities are important to understand when configuring and using Infisical SSH:
- Administrator: An individual on your team who is responsible for configuring Infisical SSH.
- Users: Other individuals on your team that need access to the remote host.
- Users: Other individuals that gain access to remote hosts through Infisical SSH.
- Host: A remote machine (e.g. EC2 instance, GCP VM, Azure VM, on-prem Linux server, Raspberry Pi, VMware VM, etc.) that users need SSH access to that is registered with Infisical SSH.
## Workflow
@@ -72,7 +72,7 @@ we will register a remote host with Infisical through a [machine identity](/docu
Next, use the `infisical ssh add-host` command to register the remote host with Infisical. As part of this command, input the ID of the Infisical SSH project you created in step 1 for the `--projectId` flag and the hostname of the remote host for the `--hostname` flag.
```bash
sudo infisical ssh add-host --projectId=<project-id> --hostname=<hostname> --token="$INFISICAL_TOKEN" --writeUserCaToFile --writeHostCertToFile --configureSshd
sudo infisical ssh add-host --projectId=<project-id> --hostname=<hostname> --token="$INFISICAL_TOKEN" --write-user-ca-to-file --write-host-cert-to-file --configure-sshd
```
<Tip>
@@ -136,44 +136,66 @@ Once Infisical SSH is configured by an administrator, users can SSH to the remot
<Step title="Install the Infisical CLI">
Follow the instructions [here](/cli/overview) to install the Infisical CLI onto your local machine.
</Step>
<Step title="Log in with the CLI">
Run the `infisical login` command to authenticate with Infisical.
```bash
infisical login
```
</Step>
<Step title="Connect to the remote host">
Run the `infisical ssh connect` command to connect to a remote host.
The `infisical ssh connect` command can be used in either interactive or non-interactive mode to connect to a remote host.
```bash
infisical ssh connect
```
<Tabs>
<Tab title="Interactive Mode">
In interactive mode, you'll first need to authenticate with Infisical by running:
You'll be prompted to select an SSH Host from a list of accessible hosts; this is based on project membership and login mappings configured on hosts by
the administrator.
```bash
infisical login
```
```bash
Use the arrow keys to navigate: ↓ ↑ → ←
? Select an SSH Host:
▸ ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com
```
Then simply run:
After selecting a host, you'll be prompted to select a login user from a list of allowed login users:
```bash
infisical ssh connect
```
```bash
? Select Login User:
▸ ec2-user
```
You'll be prompted to select an SSH Host from a list of accessible hosts; this is based on project membership and login mappings configured on hosts by
the administrator.
If successful, you should be able to SSH to the remote host.
```bash
Use the arrow keys to navigate: ↓ ↑ → ←
? Select an SSH Host:
▸ ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com
```
```bash
✔ ec2-54-199-104-116.ap-northeast-1.compute.amazonaws.com
✔ ec2-user
✔ SSH credentials successfully added to agent
Connecting to ec2-user@ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com...
```
After selecting a host, you'll be prompted to select a login user from a list of allowed login users:
```bash
? Select Login User:
▸ ec2-user
```
If successful, you should be able to SSH to the remote host.
```bash
✔ ec2-54-199-104-116.ap-northeast-1.compute.amazonaws.com
✔ ec2-user
✔ SSH credentials successfully added to agent
Connecting to ec2-user@ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com...
```
</Tab>
<Tab title="Non-Interactive Mode">
For CI/CD pipelines or automation scenarios, you can use the non-interactive mode with an Infisical token:
```bash
infisical ssh connect \
--hostname ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com \
--login-user ec2-user \
--out-file-path ~/.ssh/id_rsa-cert.pub \
--token <your-infisical-token>
```
This will:
- Connect to the specified hostname
- Use the specified login user
- Write the SSH credentials to the specified path instead of adding them to the SSH agent
- Authenticate using the provided Infisical token
</Tab>
</Tabs>
</Step>
</Steps>

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

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