Merge branch 'main' into ENG-2656

This commit is contained in:
x
2025-04-25 18:46:05 -04:00
153 changed files with 2755 additions and 304 deletions

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

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

@@ -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 {
@@ -79,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()
@@ -118,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 };

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

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

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 950 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

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