mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-02 16:55:02 +00:00
Compare commits
39 Commits
ssh-non-in
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
632572f7c3 | |||
a524690d01 | |||
f93edbb37f | |||
fa8154ecdd | |||
d977092502 | |||
cceb29b93a | |||
02b44365f1 | |||
b506393765 | |||
204269a10d | |||
cf1f83aaa3 | |||
7894181234 | |||
04b20ed11d | |||
7a4a877e39 | |||
8f670bde88 | |||
ff9011c899 | |||
57c96abe03 | |||
7699705334 | |||
7c49f6e302 | |||
b329b5aa4b | |||
0882c181d0 | |||
8672dd641a | |||
e0dc2dd6d8 | |||
4973447676 | |||
bd2e2b7931 | |||
aa893a40a9 | |||
23df78eff8 | |||
84255d1b26 | |||
3a6b2a593b | |||
d3ee30f5e6 | |||
33dea34061 | |||
da68073e86 | |||
7bd312a287 | |||
d61e6752d6 | |||
636aee2ea9 | |||
5819b8c576 | |||
b85809293c | |||
f143d8c358 | |||
2e3330bf69 | |||
9032bbe514 |
@ -0,0 +1,19 @@
|
||||
import {
|
||||
AwsIamUserSecretRotationGeneratedCredentialsSchema,
|
||||
AwsIamUserSecretRotationSchema,
|
||||
CreateAwsIamUserSecretRotationSchema,
|
||||
UpdateAwsIamUserSecretRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||
|
||||
export const registerAwsIamUserSecretRotationRouter = async (server: FastifyZodProvider) =>
|
||||
registerSecretRotationEndpoints({
|
||||
type: SecretRotation.AwsIamUserSecret,
|
||||
server,
|
||||
responseSchema: AwsIamUserSecretRotationSchema,
|
||||
createSchema: CreateAwsIamUserSecretRotationSchema,
|
||||
updateSchema: UpdateAwsIamUserSecretRotationSchema,
|
||||
generatedCredentialsSchema: AwsIamUserSecretRotationGeneratedCredentialsSchema
|
||||
});
|
@ -1,6 +1,8 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
|
||||
import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router";
|
||||
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
|
||||
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
|
||||
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
||||
|
||||
@ -12,5 +14,7 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
|
||||
> = {
|
||||
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
|
||||
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
|
||||
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter
|
||||
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
|
||||
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
|
||||
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter
|
||||
};
|
||||
|
@ -0,0 +1,19 @@
|
||||
import {
|
||||
CreateLdapPasswordRotationSchema,
|
||||
LdapPasswordRotationGeneratedCredentialsSchema,
|
||||
LdapPasswordRotationSchema,
|
||||
UpdateLdapPasswordRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||
|
||||
export const registerLdapPasswordRotationRouter = async (server: FastifyZodProvider) =>
|
||||
registerSecretRotationEndpoints({
|
||||
type: SecretRotation.LdapPassword,
|
||||
server,
|
||||
responseSchema: LdapPasswordRotationSchema,
|
||||
createSchema: CreateLdapPasswordRotationSchema,
|
||||
updateSchema: UpdateLdapPasswordRotationSchema,
|
||||
generatedCredentialsSchema: LdapPasswordRotationGeneratedCredentialsSchema
|
||||
});
|
@ -2,6 +2,8 @@ import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||
import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
|
||||
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||
@ -13,7 +15,9 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationListItemSchema,
|
||||
MsSqlCredentialsRotationListItemSchema,
|
||||
Auth0ClientSecretRotationListItemSchema
|
||||
Auth0ClientSecretRotationListItemSchema,
|
||||
LdapPasswordRotationListItemSchema,
|
||||
AwsIamUserSecretRotationListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||
name: "AWS IAM User Secret",
|
||||
type: SecretRotation.AwsIamUserSecret,
|
||||
connection: AppConnection.AWS,
|
||||
template: {
|
||||
secretsMapping: {
|
||||
accessKeyId: "AWS_ACCESS_KEY_ID",
|
||||
secretAccessKey: "AWS_SECRET_ACCESS_KEY"
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,123 @@
|
||||
import AWS from "aws-sdk";
|
||||
|
||||
import {
|
||||
TAwsIamUserSecretRotationGeneratedCredentials,
|
||||
TAwsIamUserSecretRotationWithConnection
|
||||
} from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-types";
|
||||
import {
|
||||
TRotationFactory,
|
||||
TRotationFactoryGetSecretsPayload,
|
||||
TRotationFactoryIssueCredentials,
|
||||
TRotationFactoryRevokeCredentials,
|
||||
TRotationFactoryRotateCredentials
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws";
|
||||
|
||||
const getCreateDate = (key: AWS.IAM.AccessKeyMetadata): number => {
|
||||
return key.CreateDate ? new Date(key.CreateDate).getTime() : 0;
|
||||
};
|
||||
|
||||
export const awsIamUserSecretRotationFactory: TRotationFactory<
|
||||
TAwsIamUserSecretRotationWithConnection,
|
||||
TAwsIamUserSecretRotationGeneratedCredentials
|
||||
> = (secretRotation) => {
|
||||
const {
|
||||
parameters: { region, userName },
|
||||
connection,
|
||||
secretsMapping
|
||||
} = secretRotation;
|
||||
|
||||
const $rotateClientSecret = async () => {
|
||||
const { credentials } = await getAwsConnectionConfig(connection, region);
|
||||
const iam = new AWS.IAM({ credentials });
|
||||
|
||||
const { AccessKeyMetadata } = await iam.listAccessKeys({ UserName: userName }).promise();
|
||||
|
||||
if (AccessKeyMetadata && AccessKeyMetadata.length > 0) {
|
||||
// Sort keys by creation date (oldest first)
|
||||
const sortedKeys = [...AccessKeyMetadata].sort((a, b) => getCreateDate(a) - getCreateDate(b));
|
||||
|
||||
// If we already have 2 keys, delete the oldest one
|
||||
if (sortedKeys.length >= 2) {
|
||||
const accessId = sortedKeys[0].AccessKeyId || sortedKeys[1].AccessKeyId;
|
||||
if (accessId) {
|
||||
await iam
|
||||
.deleteAccessKey({
|
||||
UserName: userName,
|
||||
AccessKeyId: accessId
|
||||
})
|
||||
.promise();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { AccessKey } = await iam.createAccessKey({ UserName: userName }).promise();
|
||||
|
||||
return {
|
||||
accessKeyId: AccessKey.AccessKeyId,
|
||||
secretAccessKey: AccessKey.SecretAccessKey
|
||||
};
|
||||
};
|
||||
|
||||
const issueCredentials: TRotationFactoryIssueCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotateClientSecret();
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
const revokeCredentials: TRotationFactoryRevokeCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
|
||||
generatedCredentials,
|
||||
callback
|
||||
) => {
|
||||
const { credentials } = await getAwsConnectionConfig(connection, region);
|
||||
const iam = new AWS.IAM({ credentials });
|
||||
|
||||
await Promise.all(
|
||||
generatedCredentials.map((generatedCredential) =>
|
||||
iam
|
||||
.deleteAccessKey({
|
||||
UserName: userName,
|
||||
AccessKeyId: generatedCredential.accessKeyId
|
||||
})
|
||||
.promise()
|
||||
)
|
||||
);
|
||||
|
||||
return callback();
|
||||
};
|
||||
|
||||
const rotateCredentials: TRotationFactoryRotateCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
|
||||
_,
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotateClientSecret();
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TAwsIamUserSecretRotationGeneratedCredentials> = (
|
||||
generatedCredentials
|
||||
) => {
|
||||
const secrets = [
|
||||
{
|
||||
key: secretsMapping.accessKeyId,
|
||||
value: generatedCredentials.accessKeyId
|
||||
},
|
||||
{
|
||||
key: secretsMapping.secretAccessKey,
|
||||
value: generatedCredentials.secretAccessKey
|
||||
}
|
||||
];
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
||||
return {
|
||||
issueCredentials,
|
||||
revokeCredentials,
|
||||
rotateCredentials,
|
||||
getSecretsPayload
|
||||
};
|
||||
};
|
@ -0,0 +1,68 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
BaseCreateSecretRotationSchema,
|
||||
BaseSecretRotationSchema,
|
||||
BaseUpdateSecretRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const AwsIamUserSecretRotationGeneratedCredentialsSchema = z
|
||||
.object({
|
||||
accessKeyId: z.string(),
|
||||
secretAccessKey: z.string()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.max(2);
|
||||
|
||||
const AwsIamUserSecretRotationParametersSchema = z.object({
|
||||
userName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client Name Required")
|
||||
.describe(SecretRotations.PARAMETERS.AWS_IAM_USER_SECRET.userName),
|
||||
region: z.nativeEnum(AWSRegion).describe(SecretRotations.PARAMETERS.AWS_IAM_USER_SECRET.region).optional()
|
||||
});
|
||||
|
||||
const AwsIamUserSecretRotationSecretsMappingSchema = z.object({
|
||||
accessKeyId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AWS_IAM_USER_SECRET.accessKeyId),
|
||||
secretAccessKey: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AWS_IAM_USER_SECRET.secretAccessKey)
|
||||
});
|
||||
|
||||
export const AwsIamUserSecretRotationTemplateSchema = z.object({
|
||||
secretsMapping: z.object({
|
||||
accessKeyId: z.string(),
|
||||
secretAccessKey: z.string()
|
||||
})
|
||||
});
|
||||
|
||||
export const AwsIamUserSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.AwsIamUserSecret).extend({
|
||||
type: z.literal(SecretRotation.AwsIamUserSecret),
|
||||
parameters: AwsIamUserSecretRotationParametersSchema,
|
||||
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const CreateAwsIamUserSecretRotationSchema = BaseCreateSecretRotationSchema(
|
||||
SecretRotation.AwsIamUserSecret
|
||||
).extend({
|
||||
parameters: AwsIamUserSecretRotationParametersSchema,
|
||||
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const UpdateAwsIamUserSecretRotationSchema = BaseUpdateSecretRotationSchema(
|
||||
SecretRotation.AwsIamUserSecret
|
||||
).extend({
|
||||
parameters: AwsIamUserSecretRotationParametersSchema.optional(),
|
||||
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema.optional()
|
||||
});
|
||||
|
||||
export const AwsIamUserSecretRotationListItemSchema = z.object({
|
||||
name: z.literal("AWS IAM User Secret"),
|
||||
connection: z.literal(AppConnection.AWS),
|
||||
type: z.literal(SecretRotation.AwsIamUserSecret),
|
||||
template: AwsIamUserSecretRotationTemplateSchema
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TAwsConnection } from "@app/services/app-connection/aws";
|
||||
|
||||
import {
|
||||
AwsIamUserSecretRotationGeneratedCredentialsSchema,
|
||||
AwsIamUserSecretRotationListItemSchema,
|
||||
AwsIamUserSecretRotationSchema,
|
||||
CreateAwsIamUserSecretRotationSchema
|
||||
} from "./aws-iam-user-secret-rotation-schemas";
|
||||
|
||||
export type TAwsIamUserSecretRotation = z.infer<typeof AwsIamUserSecretRotationSchema>;
|
||||
|
||||
export type TAwsIamUserSecretRotationInput = z.infer<typeof CreateAwsIamUserSecretRotationSchema>;
|
||||
|
||||
export type TAwsIamUserSecretRotationListItem = z.infer<typeof AwsIamUserSecretRotationListItemSchema>;
|
||||
|
||||
export type TAwsIamUserSecretRotationWithConnection = TAwsIamUserSecretRotation & {
|
||||
connection: TAwsConnection;
|
||||
};
|
||||
|
||||
export type TAwsIamUserSecretRotationGeneratedCredentials = z.infer<
|
||||
typeof AwsIamUserSecretRotationGeneratedCredentialsSchema
|
||||
>;
|
@ -0,0 +1,3 @@
|
||||
export * from "./aws-iam-user-secret-rotation-constants";
|
||||
export * from "./aws-iam-user-secret-rotation-schemas";
|
||||
export * from "./aws-iam-user-secret-rotation-types";
|
@ -0,0 +1,3 @@
|
||||
export * from "./ldap-password-rotation-constants";
|
||||
export * from "./ldap-password-rotation-schemas";
|
||||
export * from "./ldap-password-rotation-types";
|
@ -0,0 +1,15 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const LDAP_PASSWORD_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||
name: "LDAP Password",
|
||||
type: SecretRotation.LdapPassword,
|
||||
connection: AppConnection.LDAP,
|
||||
template: {
|
||||
secretsMapping: {
|
||||
dn: "LDAP_DN",
|
||||
password: "LDAP_PASSWORD"
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,181 @@
|
||||
import ldap from "ldapjs";
|
||||
|
||||
import {
|
||||
TRotationFactory,
|
||||
TRotationFactoryGetSecretsPayload,
|
||||
TRotationFactoryIssueCredentials,
|
||||
TRotationFactoryRevokeCredentials,
|
||||
TRotationFactoryRotateCredentials
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap";
|
||||
|
||||
import { generatePassword } from "../shared/utils";
|
||||
import {
|
||||
TLdapPasswordRotationGeneratedCredentials,
|
||||
TLdapPasswordRotationWithConnection
|
||||
} from "./ldap-password-rotation-types";
|
||||
|
||||
const getEncodedPassword = (password: string) => Buffer.from(`"${password}"`, "utf16le");
|
||||
|
||||
export const ldapPasswordRotationFactory: TRotationFactory<
|
||||
TLdapPasswordRotationWithConnection,
|
||||
TLdapPasswordRotationGeneratedCredentials
|
||||
> = (secretRotation, appConnectionDAL, kmsService) => {
|
||||
const {
|
||||
connection,
|
||||
parameters: { dn, passwordRequirements },
|
||||
secretsMapping
|
||||
} = secretRotation;
|
||||
|
||||
const $verifyCredentials = async (credentials: Pick<TLdapConnection["credentials"], "dn" | "password">) => {
|
||||
try {
|
||||
const client = await getLdapConnectionClient({ ...connection.credentials, ...credentials });
|
||||
|
||||
client.unbind();
|
||||
client.destroy();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to verify credentials - ${(error as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const $rotatePassword = async () => {
|
||||
const { credentials, orgId } = connection;
|
||||
|
||||
if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection");
|
||||
|
||||
const client = await getLdapConnectionClient(credentials);
|
||||
const isPersonalRotation = credentials.dn === dn;
|
||||
|
||||
const password = generatePassword(passwordRequirements);
|
||||
|
||||
let changes: ldap.Change[] | ldap.Change;
|
||||
|
||||
switch (credentials.provider) {
|
||||
case LdapProvider.ActiveDirectory:
|
||||
{
|
||||
const encodedPassword = getEncodedPassword(password);
|
||||
|
||||
// service account vs personal password rotation require different changes
|
||||
if (isPersonalRotation) {
|
||||
const currentEncodedPassword = getEncodedPassword(credentials.password);
|
||||
|
||||
changes = [
|
||||
new ldap.Change({
|
||||
operation: "delete",
|
||||
modification: {
|
||||
type: "unicodePwd",
|
||||
values: [currentEncodedPassword]
|
||||
}
|
||||
}),
|
||||
new ldap.Change({
|
||||
operation: "add",
|
||||
modification: {
|
||||
type: "unicodePwd",
|
||||
values: [encodedPassword]
|
||||
}
|
||||
})
|
||||
];
|
||||
} else {
|
||||
changes = new ldap.Change({
|
||||
operation: "replace",
|
||||
modification: {
|
||||
type: "unicodePwd",
|
||||
values: [encodedPassword]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled provider: ${credentials.provider as LdapProvider}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.modify(dn, changes, (err) => {
|
||||
if (err) {
|
||||
logger.error(err, "LDAP Password Rotation Failed");
|
||||
reject(new Error(`Provider Modify Error: ${err.message}`));
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
client.unbind();
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
await $verifyCredentials({ dn, password });
|
||||
|
||||
if (isPersonalRotation) {
|
||||
const updatedCredentials: TLdapConnection["credentials"] = {
|
||||
...credentials,
|
||||
password
|
||||
};
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(connection.id, { encryptedCredentials });
|
||||
}
|
||||
|
||||
return { dn, password };
|
||||
};
|
||||
|
||||
const issueCredentials: TRotationFactoryIssueCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotatePassword();
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
const revokeCredentials: TRotationFactoryRevokeCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
|
||||
_,
|
||||
callback
|
||||
) => {
|
||||
// we just rotate to a new password, essentially revoking old credentials
|
||||
await $rotatePassword();
|
||||
|
||||
return callback();
|
||||
};
|
||||
|
||||
const rotateCredentials: TRotationFactoryRotateCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
|
||||
_,
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotatePassword();
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TLdapPasswordRotationGeneratedCredentials> = (
|
||||
generatedCredentials
|
||||
) => {
|
||||
const secrets = [
|
||||
{
|
||||
key: secretsMapping.dn,
|
||||
value: generatedCredentials.dn
|
||||
},
|
||||
{
|
||||
key: secretsMapping.password,
|
||||
value: generatedCredentials.password
|
||||
}
|
||||
];
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
||||
return {
|
||||
issueCredentials,
|
||||
revokeCredentials,
|
||||
rotateCredentials,
|
||||
getSecretsPayload
|
||||
};
|
||||
};
|
@ -0,0 +1,68 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
BaseCreateSecretRotationSchema,
|
||||
BaseSecretRotationSchema,
|
||||
BaseUpdateSecretRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||
import { PasswordRequirementsSchema } from "@app/ee/services/secret-rotation-v2/shared/general";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { DistinguishedNameRegex } from "@app/lib/regex";
|
||||
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const LdapPasswordRotationGeneratedCredentialsSchema = z
|
||||
.object({
|
||||
dn: z.string(),
|
||||
password: z.string()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.max(2);
|
||||
|
||||
const LdapPasswordRotationParametersSchema = z.object({
|
||||
dn: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com")
|
||||
.min(1, "Distinguished Name (DN) Required")
|
||||
.describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.dn),
|
||||
passwordRequirements: PasswordRequirementsSchema.optional()
|
||||
});
|
||||
|
||||
const LdapPasswordRotationSecretsMappingSchema = z.object({
|
||||
dn: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.dn),
|
||||
password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.password)
|
||||
});
|
||||
|
||||
export const LdapPasswordRotationTemplateSchema = z.object({
|
||||
secretsMapping: z.object({
|
||||
dn: z.string(),
|
||||
password: z.string()
|
||||
})
|
||||
});
|
||||
|
||||
export const LdapPasswordRotationSchema = BaseSecretRotationSchema(SecretRotation.LdapPassword).extend({
|
||||
type: z.literal(SecretRotation.LdapPassword),
|
||||
parameters: LdapPasswordRotationParametersSchema,
|
||||
secretsMapping: LdapPasswordRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword).extend({
|
||||
parameters: LdapPasswordRotationParametersSchema,
|
||||
secretsMapping: LdapPasswordRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const UpdateLdapPasswordRotationSchema = BaseUpdateSecretRotationSchema(SecretRotation.LdapPassword).extend({
|
||||
parameters: LdapPasswordRotationParametersSchema.optional(),
|
||||
secretsMapping: LdapPasswordRotationSecretsMappingSchema.optional()
|
||||
});
|
||||
|
||||
export const LdapPasswordRotationListItemSchema = z.object({
|
||||
name: z.literal("LDAP Password"),
|
||||
connection: z.literal(AppConnection.LDAP),
|
||||
type: z.literal(SecretRotation.LdapPassword),
|
||||
template: LdapPasswordRotationTemplateSchema
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TLdapConnection } from "@app/services/app-connection/ldap";
|
||||
|
||||
import {
|
||||
CreateLdapPasswordRotationSchema,
|
||||
LdapPasswordRotationGeneratedCredentialsSchema,
|
||||
LdapPasswordRotationListItemSchema,
|
||||
LdapPasswordRotationSchema
|
||||
} from "./ldap-password-rotation-schemas";
|
||||
|
||||
export type TLdapPasswordRotation = z.infer<typeof LdapPasswordRotationSchema>;
|
||||
|
||||
export type TLdapPasswordRotationInput = z.infer<typeof CreateLdapPasswordRotationSchema>;
|
||||
|
||||
export type TLdapPasswordRotationListItem = z.infer<typeof LdapPasswordRotationListItemSchema>;
|
||||
|
||||
export type TLdapPasswordRotationWithConnection = TLdapPasswordRotation & {
|
||||
connection: TLdapConnection;
|
||||
};
|
||||
|
||||
export type TLdapPasswordRotationGeneratedCredentials = z.infer<typeof LdapPasswordRotationGeneratedCredentialsSchema>;
|
@ -1,7 +1,9 @@
|
||||
export enum SecretRotation {
|
||||
PostgresCredentials = "postgres-credentials",
|
||||
MsSqlCredentials = "mssql-credentials",
|
||||
Auth0ClientSecret = "auth0-client-secret"
|
||||
Auth0ClientSecret = "auth0-client-secret",
|
||||
LdapPassword = "ldap-password",
|
||||
AwsIamUserSecret = "aws-iam-user-secret"
|
||||
}
|
||||
|
||||
export enum SecretRotationStatus {
|
||||
|
@ -4,6 +4,8 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
|
||||
import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret";
|
||||
import { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password";
|
||||
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
|
||||
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
||||
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
|
||||
@ -18,7 +20,9 @@ import {
|
||||
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
|
||||
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION
|
||||
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretRotationOptions = () => {
|
||||
|
@ -3,12 +3,16 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
||||
|
||||
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
||||
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials",
|
||||
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret"
|
||||
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials",
|
||||
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
|
||||
[SecretRotation.LdapPassword]: "LDAP Password",
|
||||
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret"
|
||||
};
|
||||
|
||||
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
|
||||
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0
|
||||
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
|
||||
[SecretRotation.LdapPassword]: AppConnection.LDAP,
|
||||
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns";
|
||||
import { ldapPasswordRotationFactory } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns";
|
||||
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
calculateNextRotationAt,
|
||||
@ -77,6 +78,7 @@ import {
|
||||
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||
|
||||
import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns";
|
||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||
|
||||
export type TSecretRotationV2ServiceFactoryDep = {
|
||||
@ -114,7 +116,9 @@ type TRotationFactoryImplementation = TRotationFactory<
|
||||
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
|
||||
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation
|
||||
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation
|
||||
};
|
||||
|
||||
export const secretRotationV2ServiceFactory = ({
|
||||
@ -449,6 +453,18 @@ export const secretRotationV2ServiceFactory = ({
|
||||
kmsService
|
||||
);
|
||||
|
||||
// even though we have a db constraint we want to check before any rotation of credentials is attempted
|
||||
// to prevent creation failure after external credentials have been modified
|
||||
const conflictingRotation = await secretRotationV2DAL.findOne({
|
||||
name: payload.name,
|
||||
folderId: folder.id
|
||||
});
|
||||
|
||||
if (conflictingRotation)
|
||||
throw new BadRequestError({
|
||||
message: `A Secret Rotation with the name "${payload.name}" already exists at the secret path "${secretPath}"`
|
||||
});
|
||||
|
||||
try {
|
||||
const currentTime = new Date();
|
||||
|
||||
|
@ -12,6 +12,20 @@ import {
|
||||
TAuth0ClientSecretRotationListItem,
|
||||
TAuth0ClientSecretRotationWithConnection
|
||||
} from "./auth0-client-secret";
|
||||
import {
|
||||
TAwsIamUserSecretRotation,
|
||||
TAwsIamUserSecretRotationGeneratedCredentials,
|
||||
TAwsIamUserSecretRotationInput,
|
||||
TAwsIamUserSecretRotationListItem,
|
||||
TAwsIamUserSecretRotationWithConnection
|
||||
} from "./aws-iam-user-secret";
|
||||
import {
|
||||
TLdapPasswordRotation,
|
||||
TLdapPasswordRotationGeneratedCredentials,
|
||||
TLdapPasswordRotationInput,
|
||||
TLdapPasswordRotationListItem,
|
||||
TLdapPasswordRotationWithConnection
|
||||
} from "./ldap-password";
|
||||
import {
|
||||
TMsSqlCredentialsRotation,
|
||||
TMsSqlCredentialsRotationInput,
|
||||
@ -27,26 +41,39 @@ import {
|
||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||
import { SecretRotation } from "./secret-rotation-v2-enums";
|
||||
|
||||
export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation;
|
||||
export type TSecretRotationV2 =
|
||||
| TPostgresCredentialsRotation
|
||||
| TMsSqlCredentialsRotation
|
||||
| TAuth0ClientSecretRotation
|
||||
| TLdapPasswordRotation
|
||||
| TAwsIamUserSecretRotation;
|
||||
|
||||
export type TSecretRotationV2WithConnection =
|
||||
| TPostgresCredentialsRotationWithConnection
|
||||
| TMsSqlCredentialsRotationWithConnection
|
||||
| TAuth0ClientSecretRotationWithConnection;
|
||||
| TAuth0ClientSecretRotationWithConnection
|
||||
| TLdapPasswordRotationWithConnection
|
||||
| TAwsIamUserSecretRotationWithConnection;
|
||||
|
||||
export type TSecretRotationV2GeneratedCredentials =
|
||||
| TSqlCredentialsRotationGeneratedCredentials
|
||||
| TAuth0ClientSecretRotationGeneratedCredentials;
|
||||
| TAuth0ClientSecretRotationGeneratedCredentials
|
||||
| TLdapPasswordRotationGeneratedCredentials
|
||||
| TAwsIamUserSecretRotationGeneratedCredentials;
|
||||
|
||||
export type TSecretRotationV2Input =
|
||||
| TPostgresCredentialsRotationInput
|
||||
| TMsSqlCredentialsRotationInput
|
||||
| TAuth0ClientSecretRotationInput;
|
||||
| TAuth0ClientSecretRotationInput
|
||||
| TLdapPasswordRotationInput
|
||||
| TAwsIamUserSecretRotationInput;
|
||||
|
||||
export type TSecretRotationV2ListItem =
|
||||
| TPostgresCredentialsRotationListItem
|
||||
| TMsSqlCredentialsRotationListItem
|
||||
| TAuth0ClientSecretRotationListItem;
|
||||
| TAuth0ClientSecretRotationListItem
|
||||
| TLdapPasswordRotationListItem
|
||||
| TAwsIamUserSecretRotationListItem;
|
||||
|
||||
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
|
||||
|
||||
|
@ -1,11 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||
import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
|
||||
import { AwsIamUserSecretRotationSchema } from "./aws-iam-user-secret";
|
||||
|
||||
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationSchema,
|
||||
MsSqlCredentialsRotationSchema,
|
||||
Auth0ClientSecretRotationSchema
|
||||
Auth0ClientSecretRotationSchema,
|
||||
LdapPasswordRotationSchema,
|
||||
AwsIamUserSecretRotationSchema
|
||||
]);
|
||||
|
@ -0,0 +1 @@
|
||||
export * from "./password-requirements-schema";
|
@ -0,0 +1,44 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
|
||||
export const PasswordRequirementsSchema = z
|
||||
.object({
|
||||
length: z
|
||||
.number()
|
||||
.min(1, "Password length must be a positive number")
|
||||
.max(250, "Password length must be less than 250")
|
||||
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.length),
|
||||
required: z.object({
|
||||
digits: z
|
||||
.number()
|
||||
.min(0, "Digit count must be non-negative")
|
||||
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.digits),
|
||||
lowercase: z
|
||||
.number()
|
||||
.min(0, "Lowercase count must be non-negative")
|
||||
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.lowercase),
|
||||
uppercase: z
|
||||
.number()
|
||||
.min(0, "Uppercase count must be non-negative")
|
||||
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.uppercase),
|
||||
symbols: z
|
||||
.number()
|
||||
.min(0, "Symbol count must be non-negative")
|
||||
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.symbols)
|
||||
}),
|
||||
allowedSymbols: z
|
||||
.string()
|
||||
.regex(new RE2("[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?~]"), "Invalid symbols")
|
||||
.optional()
|
||||
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.allowedSymbols)
|
||||
})
|
||||
.refine((data) => {
|
||||
return Object.values(data.required).some((count) => count > 0);
|
||||
}, "At least one character type must be required")
|
||||
.refine((data) => {
|
||||
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
|
||||
return total <= data.length;
|
||||
}, "Sum of required characters cannot exceed the total length")
|
||||
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.base);
|
@ -1,6 +1,17 @@
|
||||
import { randomInt } from "crypto";
|
||||
|
||||
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||
type TPasswordRequirements = {
|
||||
length: number;
|
||||
required: {
|
||||
lowercase: number;
|
||||
uppercase: number;
|
||||
digits: number;
|
||||
symbols: number;
|
||||
};
|
||||
allowedSymbols?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
|
||||
length: 48,
|
||||
required: {
|
||||
lowercase: 1,
|
||||
@ -11,9 +22,9 @@ const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||
allowedSymbols: "-_.~!*"
|
||||
};
|
||||
|
||||
export const generatePassword = () => {
|
||||
export const generatePassword = (passwordRequirements?: TPasswordRequirements) => {
|
||||
try {
|
||||
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
|
||||
const { length, required, allowedSymbols } = passwordRequirements ?? DEFAULT_PASSWORD_REQUIREMENTS;
|
||||
|
||||
const chars = {
|
||||
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||
|
@ -1857,6 +1857,20 @@ export const AppConnections = {
|
||||
WINDMILL: {
|
||||
instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).",
|
||||
accessToken: "The access token to use to connect with Windmill."
|
||||
},
|
||||
LDAP: {
|
||||
provider: "The type of LDAP provider. Determines provider-specific behaviors.",
|
||||
url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').",
|
||||
dn: "The Distinguished Name (DN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com').",
|
||||
password: "The password to bind with for authentication.",
|
||||
sslRejectUnauthorized:
|
||||
"Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.",
|
||||
sslCertificate:
|
||||
"The SSL certificate (PEM format) to use for secure connection when using ldaps:// with a self-signed certificate."
|
||||
},
|
||||
TEAMCITY: {
|
||||
instanceUrl: "The TeamCity instance URL to connect with.",
|
||||
accessToken: "The access token to use to connect with TeamCity."
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1996,6 +2010,10 @@ export const SecretSyncs = {
|
||||
WINDMILL: {
|
||||
workspace: "The Windmill workspace to sync secrets to.",
|
||||
path: "The Windmill workspace path to sync secrets to."
|
||||
},
|
||||
TEAMCITY: {
|
||||
project: "The TeamCity project to sync secrets to.",
|
||||
buildConfig: "The TeamCity build configuration to sync secrets to."
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -2060,6 +2078,26 @@ export const SecretRotations = {
|
||||
},
|
||||
AUTH0_CLIENT_SECRET: {
|
||||
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
|
||||
},
|
||||
LDAP_PASSWORD: {
|
||||
dn: "The Distinguished Name (DN) of the principal to rotate the password for."
|
||||
},
|
||||
GENERAL: {
|
||||
PASSWORD_REQUIREMENTS: {
|
||||
base: "The password requirements to use when generating the new password.",
|
||||
length: "The length of the password to generate.",
|
||||
required: {
|
||||
digits: "The amount of digits to require in the generated password.",
|
||||
lowercase: "The amount of lowercase characters to require in the generated password.",
|
||||
uppercase: "The amount of uppercase characters to require in the generated password.",
|
||||
symbols: "The amount of symbols to require in the generated password."
|
||||
},
|
||||
allowedSymbols: 'The allowed symbols to use in the generated password (defaults to "-_.~!*").'
|
||||
}
|
||||
},
|
||||
AWS_IAM_USER_SECRET: {
|
||||
userName: "The name of the client to rotate credentials for.",
|
||||
region: "The AWS region the client is present in."
|
||||
}
|
||||
},
|
||||
SECRETS_MAPPING: {
|
||||
@ -2070,6 +2108,14 @@ export const SecretRotations = {
|
||||
AUTH0_CLIENT_SECRET: {
|
||||
clientId: "The name of the secret that the client ID will be mapped to.",
|
||||
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
|
||||
},
|
||||
LDAP_PASSWORD: {
|
||||
dn: "The name of the secret that the Distinguished Name (DN) of the principal will be mapped to.",
|
||||
password: "The name of the secret that the rotated password will be mapped to."
|
||||
},
|
||||
AWS_IAM_USER_SECRET: {
|
||||
accessKeyId: "The name of the secret that the access key ID will be mapped to.",
|
||||
secretAccessKey: "The name of the secret that the rotated secret access key will be mapped to."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
3
backend/src/lib/regex/index.ts
Normal file
3
backend/src/lib/regex/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const DistinguishedNameRegex =
|
||||
// DN format, ie; CN=user,OU=users,DC=example,DC=com
|
||||
/^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/;
|
@ -28,11 +28,16 @@ import {
|
||||
HumanitecConnectionListItemSchema,
|
||||
SanitizedHumanitecConnectionSchema
|
||||
} from "@app/services/app-connection/humanitec";
|
||||
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
|
||||
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
|
||||
import {
|
||||
PostgresConnectionListItemSchema,
|
||||
SanitizedPostgresConnectionSchema
|
||||
} from "@app/services/app-connection/postgres";
|
||||
import {
|
||||
SanitizedTeamCityConnectionSchema,
|
||||
TeamCityConnectionListItemSchema
|
||||
} from "@app/services/app-connection/teamcity";
|
||||
import {
|
||||
SanitizedTerraformCloudConnectionSchema,
|
||||
TerraformCloudConnectionListItemSchema
|
||||
@ -59,7 +64,9 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedMsSqlConnectionSchema.options,
|
||||
...SanitizedCamundaConnectionSchema.options,
|
||||
...SanitizedWindmillConnectionSchema.options,
|
||||
...SanitizedAuth0ConnectionSchema.options
|
||||
...SanitizedAuth0ConnectionSchema.options,
|
||||
...SanitizedLdapConnectionSchema.options,
|
||||
...SanitizedTeamCityConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@ -76,7 +83,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
MsSqlConnectionListItemSchema,
|
||||
CamundaConnectionListItemSchema,
|
||||
WindmillConnectionListItemSchema,
|
||||
Auth0ConnectionListItemSchema
|
||||
Auth0ConnectionListItemSchema,
|
||||
LdapConnectionListItemSchema,
|
||||
TeamCityConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { registerAuth0ConnectionRouter } from "@app/server/routes/v1/app-connection-routers/auth0-connection-router";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
|
||||
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
|
||||
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
|
||||
@ -9,8 +9,10 @@ import { registerDatabricksConnectionRouter } from "./databricks-connection-rout
|
||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
||||
import { registerLdapConnectionRouter } from "./ldap-connection-router";
|
||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
|
||||
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
|
||||
import { registerVercelConnectionRouter } from "./vercel-connection-router";
|
||||
import { registerWindmillConnectionRouter } from "./windmill-connection-router";
|
||||
@ -32,5 +34,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
|
||||
[AppConnection.Camunda]: registerCamundaConnectionRouter,
|
||||
[AppConnection.Windmill]: registerWindmillConnectionRouter,
|
||||
[AppConnection.Auth0]: registerAuth0ConnectionRouter
|
||||
[AppConnection.Auth0]: registerAuth0ConnectionRouter,
|
||||
[AppConnection.LDAP]: registerLdapConnectionRouter,
|
||||
[AppConnection.TeamCity]: registerTeamCityConnectionRouter
|
||||
};
|
||||
|
@ -0,0 +1,18 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateLdapConnectionSchema,
|
||||
SanitizedLdapConnectionSchema,
|
||||
UpdateLdapConnectionSchema
|
||||
} from "@app/services/app-connection/ldap";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerLdapConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.LDAP,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedLdapConnectionSchema,
|
||||
createSchema: CreateLdapConnectionSchema,
|
||||
updateSchema: UpdateLdapConnectionSchema
|
||||
});
|
||||
};
|
@ -0,0 +1,60 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateTeamCityConnectionSchema,
|
||||
SanitizedTeamCityConnectionSchema,
|
||||
UpdateTeamCityConnectionSchema
|
||||
} from "@app/services/app-connection/teamcity";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerTeamCityConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.TeamCity,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedTeamCityConnectionSchema,
|
||||
createSchema: CreateTeamCityConnectionSchema,
|
||||
updateSchema: UpdateTeamCityConnectionSchema
|
||||
});
|
||||
|
||||
// The following endpoints are for internal Infisical App use only and not part of the public API
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/projects`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
buildTypes: z.object({
|
||||
buildType: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const projects = await server.services.appConnection.teamcity.listProjects(connectionId, req.permission);
|
||||
|
||||
return projects;
|
||||
}
|
||||
});
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
@ -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 };
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
});
|
@ -12,7 +12,9 @@ export enum AppConnection {
|
||||
MsSql = "mssql",
|
||||
Camunda = "camunda",
|
||||
Windmill = "windmill",
|
||||
Auth0 = "auth0"
|
||||
Auth0 = "auth0",
|
||||
LDAP = "ldap",
|
||||
TeamCity = "teamcity"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
@ -41,8 +41,14 @@ import {
|
||||
HumanitecConnectionMethod,
|
||||
validateHumanitecConnectionCredentials
|
||||
} from "./humanitec";
|
||||
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
|
||||
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||
import {
|
||||
getTeamCityConnectionListItem,
|
||||
TeamCityConnectionMethod,
|
||||
validateTeamCityConnectionCredentials
|
||||
} from "./teamcity";
|
||||
import {
|
||||
getTerraformCloudConnectionListItem,
|
||||
TerraformCloudConnectionMethod,
|
||||
@ -71,7 +77,9 @@ export const listAppConnectionOptions = () => {
|
||||
getMsSqlConnectionListItem(),
|
||||
getCamundaConnectionListItem(),
|
||||
getWindmillConnectionListItem(),
|
||||
getAuth0ConnectionListItem()
|
||||
getAuth0ConnectionListItem(),
|
||||
getLdapConnectionListItem(),
|
||||
getTeamCityConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@ -135,7 +143,9 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
@ -167,9 +177,12 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
return "Username & Password";
|
||||
case WindmillConnectionMethod.AccessToken:
|
||||
case TeamCityConnectionMethod.AccessToken:
|
||||
return "Access Token";
|
||||
case Auth0ConnectionMethod.ClientCredentials:
|
||||
return "Client Credentials";
|
||||
case LdapConnectionMethod.SimpleBind:
|
||||
return "Simple Bind";
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
@ -214,5 +227,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
|
||||
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
@ -14,5 +14,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.MsSql]: "Microsoft SQL Server",
|
||||
[AppConnection.Camunda]: "Camunda",
|
||||
[AppConnection.Windmill]: "Windmill",
|
||||
[AppConnection.Auth0]: "Auth0"
|
||||
[AppConnection.Auth0]: "Auth0",
|
||||
[AppConnection.LDAP]: "LDAP",
|
||||
[AppConnection.TeamCity]: "TeamCity"
|
||||
};
|
||||
|
@ -43,8 +43,11 @@ import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||
import { githubConnectionService } from "./github/github-connection-service";
|
||||
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
|
||||
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
|
||||
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
|
||||
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
|
||||
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
|
||||
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
|
||||
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
|
||||
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
|
||||
@ -74,7 +77,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
||||
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
|
||||
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
|
||||
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema
|
||||
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
|
||||
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
|
||||
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@ -450,6 +455,7 @@ export const appConnectionServiceFactory = ({
|
||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
vercel: vercelConnectionService(connectAppConnectionById),
|
||||
windmill: windmillConnectionService(connectAppConnectionById),
|
||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
teamcity: teamcityConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
@ -57,12 +57,24 @@ import {
|
||||
THumanitecConnectionInput,
|
||||
TValidateHumanitecConnectionCredentialsSchema
|
||||
} from "./humanitec";
|
||||
import {
|
||||
TLdapConnection,
|
||||
TLdapConnectionConfig,
|
||||
TLdapConnectionInput,
|
||||
TValidateLdapConnectionCredentialsSchema
|
||||
} from "./ldap";
|
||||
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import {
|
||||
TPostgresConnection,
|
||||
TPostgresConnectionInput,
|
||||
TValidatePostgresConnectionCredentialsSchema
|
||||
} from "./postgres";
|
||||
import {
|
||||
TTeamCityConnection,
|
||||
TTeamCityConnectionConfig,
|
||||
TTeamCityConnectionInput,
|
||||
TValidateTeamCityConnectionCredentialsSchema
|
||||
} from "./teamcity";
|
||||
import {
|
||||
TTerraformCloudConnection,
|
||||
TTerraformCloudConnectionConfig,
|
||||
@ -97,6 +109,8 @@ export type TAppConnection = { id: string } & (
|
||||
| TCamundaConnection
|
||||
| TWindmillConnection
|
||||
| TAuth0Connection
|
||||
| TLdapConnection
|
||||
| TTeamCityConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@ -118,6 +132,8 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TCamundaConnectionInput
|
||||
| TWindmillConnectionInput
|
||||
| TAuth0ConnectionInput
|
||||
| TLdapConnectionInput
|
||||
| TTeamCityConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
|
||||
@ -144,7 +160,9 @@ export type TAppConnectionConfig =
|
||||
| TSqlConnectionConfig
|
||||
| TCamundaConnectionConfig
|
||||
| TWindmillConnectionConfig
|
||||
| TAuth0ConnectionConfig;
|
||||
| TAuth0ConnectionConfig
|
||||
| TLdapConnectionConfig
|
||||
| TTeamCityConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@ -160,7 +178,9 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateTerraformCloudConnectionCredentialsSchema
|
||||
| TValidateVercelConnectionCredentialsSchema
|
||||
| TValidateWindmillConnectionCredentialsSchema
|
||||
| TValidateAuth0ConnectionCredentialsSchema;
|
||||
| TValidateAuth0ConnectionCredentialsSchema
|
||||
| TValidateLdapConnectionCredentialsSchema
|
||||
| TValidateTeamCityConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
@ -168,6 +188,10 @@ export type TListAwsConnectionKmsKeys = {
|
||||
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
||||
};
|
||||
|
||||
export type TListAwsConnectionIamUsers = {
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
export type TAppConnectionCredentialsValidator = (
|
||||
appConnection: TAppConnectionConfig
|
||||
) => Promise<TAppConnection["credentials"]>;
|
||||
|
@ -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}`
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
};
|
||||
|
4
backend/src/services/app-connection/ldap/index.ts
Normal file
4
backend/src/services/app-connection/ldap/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./ldap-connection-enums";
|
||||
export * from "./ldap-connection-fns";
|
||||
export * from "./ldap-connection-schemas";
|
||||
export * from "./ldap-connection-types";
|
@ -0,0 +1,7 @@
|
||||
export enum LdapConnectionMethod {
|
||||
SimpleBind = "simple-bind"
|
||||
}
|
||||
|
||||
export enum LdapProvider {
|
||||
ActiveDirectory = "active-directory"
|
||||
}
|
102
backend/src/services/app-connection/ldap/ldap-connection-fns.ts
Normal file
102
backend/src/services/app-connection/ldap/ldap-connection-fns.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import ldap from "ldapjs";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { LdapConnectionMethod } from "./ldap-connection-enums";
|
||||
import { TLdapConnectionConfig } from "./ldap-connection-types";
|
||||
|
||||
export const getLdapConnectionListItem = () => {
|
||||
return {
|
||||
name: "LDAP" as const,
|
||||
app: AppConnection.LDAP as const,
|
||||
methods: Object.values(LdapConnectionMethod) as [LdapConnectionMethod.SimpleBind]
|
||||
};
|
||||
};
|
||||
|
||||
const LDAP_TIMEOUT = 15_000;
|
||||
|
||||
export const getLdapConnectionClient = async ({
|
||||
url,
|
||||
dn,
|
||||
password,
|
||||
sslCertificate,
|
||||
sslRejectUnauthorized = true
|
||||
}: TLdapConnectionConfig["credentials"]) => {
|
||||
await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
const isSSL = url.startsWith("ldaps");
|
||||
|
||||
return new Promise<ldap.Client>((resolve, reject) => {
|
||||
const client = ldap.createClient({
|
||||
url,
|
||||
timeout: LDAP_TIMEOUT,
|
||||
connectTimeout: LDAP_TIMEOUT,
|
||||
tlsOptions: isSSL
|
||||
? {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
ca: sslCertificate ? [sslCertificate] : undefined
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
|
||||
client.on("error", (err: Error) => {
|
||||
logger.error(err, "LDAP Error");
|
||||
client.destroy();
|
||||
reject(new Error(`Provider Error - ${err.message}`));
|
||||
});
|
||||
|
||||
client.on("connectError", (err: Error) => {
|
||||
logger.error(err, "LDAP Connection Error");
|
||||
client.destroy();
|
||||
reject(new Error(`Provider Connect Error - ${err.message}`));
|
||||
});
|
||||
|
||||
client.on("connectRefused", (err: Error) => {
|
||||
logger.error(err, "LDAP Connection Refused");
|
||||
client.destroy();
|
||||
reject(new Error(`Provider Connection Refused - ${err.message}`));
|
||||
});
|
||||
|
||||
client.on("connectTimeout", (err: Error) => {
|
||||
logger.error(err, "LDAP Connection Timeout");
|
||||
client.destroy();
|
||||
reject(new Error(`Provider Connection Timeout - ${err.message}`));
|
||||
});
|
||||
|
||||
client.on("connect", () => {
|
||||
client.bind(dn, password, (err) => {
|
||||
if (err) {
|
||||
logger.error(err, "LDAP Bind Error");
|
||||
reject(new Error(`Bind Error: ${err.message}`));
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
resolve(client);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => {
|
||||
let client: ldap.Client | undefined;
|
||||
|
||||
try {
|
||||
client = await getLdapConnectionClient(credentials);
|
||||
|
||||
// this shouldn't occur as handle connection error events in client but here as fallback
|
||||
if (!client.connected) {
|
||||
throw new BadRequestError({ message: "Unable to connect to LDAP server" });
|
||||
}
|
||||
|
||||
return credentials;
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection: ${(e as Error).message || "verify credentials"}`
|
||||
});
|
||||
} finally {
|
||||
client?.destroy();
|
||||
}
|
||||
};
|
@ -0,0 +1,93 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { DistinguishedNameRegex } from "@app/lib/regex";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { LdapConnectionMethod, LdapProvider } from "./ldap-connection-enums";
|
||||
|
||||
export const LdapConnectionSimpleBindCredentialsSchema = z.object({
|
||||
provider: z.nativeEnum(LdapProvider).describe(AppConnections.CREDENTIALS.LDAP.provider),
|
||||
url: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "URL required")
|
||||
.regex(new RE2(/^ldaps?:\/\//))
|
||||
.describe(AppConnections.CREDENTIALS.LDAP.url),
|
||||
dn: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com")
|
||||
.min(1, "Distinguished Name (DN) required")
|
||||
.describe(AppConnections.CREDENTIALS.LDAP.dn),
|
||||
password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.LDAP.password),
|
||||
sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.LDAP.sslRejectUnauthorized),
|
||||
sslCertificate: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((value) => value || undefined)
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.LDAP.sslCertificate)
|
||||
});
|
||||
|
||||
const BaseLdapConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.LDAP)
|
||||
});
|
||||
|
||||
export const LdapConnectionSchema = z.intersection(
|
||||
BaseLdapConnectionSchema,
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(LdapConnectionMethod.SimpleBind),
|
||||
credentials: LdapConnectionSimpleBindCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
export const SanitizedLdapConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseLdapConnectionSchema.extend({
|
||||
method: z.literal(LdapConnectionMethod.SimpleBind),
|
||||
credentials: LdapConnectionSimpleBindCredentialsSchema.pick({
|
||||
provider: true,
|
||||
url: true,
|
||||
dn: true,
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(LdapConnectionMethod.SimpleBind).describe(AppConnections.CREATE(AppConnection.LDAP).method),
|
||||
credentials: LdapConnectionSimpleBindCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.LDAP).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateLdapConnectionSchema = ValidateLdapConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.LDAP)
|
||||
);
|
||||
|
||||
export const UpdateLdapConnectionSchema = z
|
||||
.object({
|
||||
credentials: LdapConnectionSimpleBindCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.LDAP).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.LDAP));
|
||||
|
||||
export const LdapConnectionListItemSchema = z.object({
|
||||
name: z.literal("LDAP"),
|
||||
app: z.literal(AppConnection.LDAP),
|
||||
// the below is preferable but currently breaks with our zod to json schema parser
|
||||
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
|
||||
methods: z.nativeEnum(LdapConnectionMethod).array()
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import {
|
||||
CreateLdapConnectionSchema,
|
||||
LdapConnectionSchema,
|
||||
ValidateLdapConnectionCredentialsSchema
|
||||
} from "./ldap-connection-schemas";
|
||||
|
||||
export type TLdapConnection = z.infer<typeof LdapConnectionSchema>;
|
||||
|
||||
export type TLdapConnectionInput = z.infer<typeof CreateLdapConnectionSchema> & {
|
||||
app: AppConnection.LDAP;
|
||||
};
|
||||
|
||||
export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema;
|
||||
|
||||
export type TLdapConnectionConfig = DiscriminativePick<TLdapConnection, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
@ -31,7 +31,8 @@ export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [
|
||||
port: true,
|
||||
username: true,
|
||||
sslEnabled: true,
|
||||
sslRejectUnauthorized: true
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
@ -29,7 +29,8 @@ export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method",
|
||||
port: true,
|
||||
username: true,
|
||||
sslEnabled: true,
|
||||
sslRejectUnauthorized: true
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
4
backend/src/services/app-connection/teamcity/index.ts
Normal file
4
backend/src/services/app-connection/teamcity/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./teamcity-connection-enums";
|
||||
export * from "./teamcity-connection-fns";
|
||||
export * from "./teamcity-connection-schemas";
|
||||
export * from "./teamcity-connection-types";
|
@ -0,0 +1,3 @@
|
||||
export enum TeamCityConnectionMethod {
|
||||
AccessToken = "access-token"
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { TeamCityConnectionMethod } from "./teamcity-connection-enums";
|
||||
import {
|
||||
TTeamCityConnection,
|
||||
TTeamCityConnectionConfig,
|
||||
TTeamCityListProjectsResponse
|
||||
} from "./teamcity-connection-types";
|
||||
|
||||
export const getTeamCityInstanceUrl = async (config: TTeamCityConnectionConfig) => {
|
||||
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||
|
||||
return instanceUrl;
|
||||
};
|
||||
|
||||
export const getTeamCityConnectionListItem = () => {
|
||||
return {
|
||||
name: "TeamCity" as const,
|
||||
app: AppConnection.TeamCity as const,
|
||||
methods: Object.values(TeamCityConnectionMethod) as [TeamCityConnectionMethod.AccessToken]
|
||||
};
|
||||
};
|
||||
|
||||
export const validateTeamCityConnectionCredentials = async (config: TTeamCityConnectionConfig) => {
|
||||
const instanceUrl = await getTeamCityInstanceUrl(config);
|
||||
|
||||
const { accessToken } = config.credentials;
|
||||
|
||||
try {
|
||||
await request.get(`${instanceUrl}/app/rest/server`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
};
|
||||
|
||||
export const listTeamCityProjects = async (appConnection: TTeamCityConnection) => {
|
||||
const instanceUrl = await getTeamCityInstanceUrl(appConnection);
|
||||
const { accessToken } = appConnection.credentials;
|
||||
|
||||
const resp = await request.get<TTeamCityListProjectsResponse>(
|
||||
`${instanceUrl}/app/rest/projects?fields=project(id,name,buildTypes(buildType(id,name)))`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Filter out the root project. Should not be seen by users.
|
||||
return resp.data.project.filter((proj) => proj.id !== "_Root");
|
||||
};
|
@ -0,0 +1,70 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { TeamCityConnectionMethod } from "./teamcity-connection-enums";
|
||||
|
||||
export const TeamCityConnectionAccessTokenCredentialsSchema = z.object({
|
||||
accessToken: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Access Token required")
|
||||
.describe(AppConnections.CREDENTIALS.TEAMCITY.accessToken),
|
||||
instanceUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.url("Invalid Instance URL")
|
||||
.min(1, "Instance URL required")
|
||||
.describe(AppConnections.CREDENTIALS.TEAMCITY.instanceUrl)
|
||||
});
|
||||
|
||||
const BaseTeamCityConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.TeamCity) });
|
||||
|
||||
export const TeamCityConnectionSchema = BaseTeamCityConnectionSchema.extend({
|
||||
method: z.literal(TeamCityConnectionMethod.AccessToken),
|
||||
credentials: TeamCityConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedTeamCityConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseTeamCityConnectionSchema.extend({
|
||||
method: z.literal(TeamCityConnectionMethod.AccessToken),
|
||||
credentials: TeamCityConnectionAccessTokenCredentialsSchema.pick({
|
||||
instanceUrl: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateTeamCityConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(TeamCityConnectionMethod.AccessToken)
|
||||
.describe(AppConnections.CREATE(AppConnection.TeamCity).method),
|
||||
credentials: TeamCityConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.TeamCity).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateTeamCityConnectionSchema = ValidateTeamCityConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.TeamCity)
|
||||
);
|
||||
|
||||
export const UpdateTeamCityConnectionSchema = z
|
||||
.object({
|
||||
credentials: TeamCityConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.TeamCity).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TeamCity));
|
||||
|
||||
export const TeamCityConnectionListItemSchema = z.object({
|
||||
name: z.literal("TeamCity"),
|
||||
app: z.literal(AppConnection.TeamCity),
|
||||
methods: z.nativeEnum(TeamCityConnectionMethod).array()
|
||||
});
|
@ -0,0 +1,28 @@
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listTeamCityProjects } from "./teamcity-connection-fns";
|
||||
import { TTeamCityConnection } from "./teamcity-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TTeamCityConnection>;
|
||||
|
||||
export const teamcityConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.TeamCity, connectionId, actor);
|
||||
|
||||
try {
|
||||
const projects = await listTeamCityProjects(appConnection);
|
||||
return projects;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listProjects
|
||||
};
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateTeamCityConnectionSchema,
|
||||
TeamCityConnectionSchema,
|
||||
ValidateTeamCityConnectionCredentialsSchema
|
||||
} from "./teamcity-connection-schemas";
|
||||
|
||||
export type TTeamCityConnection = z.infer<typeof TeamCityConnectionSchema>;
|
||||
|
||||
export type TTeamCityConnectionInput = z.infer<typeof CreateTeamCityConnectionSchema> & {
|
||||
app: AppConnection.TeamCity;
|
||||
};
|
||||
|
||||
export type TValidateTeamCityConnectionCredentialsSchema = typeof ValidateTeamCityConnectionCredentialsSchema;
|
||||
|
||||
export type TTeamCityConnectionConfig = DiscriminativePick<
|
||||
TTeamCityConnectionInput,
|
||||
"method" | "app" | "credentials"
|
||||
> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TTeamCityProject = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TTeamCityProjectWithBuildTypes = TTeamCityProject & {
|
||||
buildTypes: {
|
||||
buildType: {
|
||||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
export type TTeamCityListProjectsResponse = {
|
||||
project: TTeamCityProjectWithBuildTypes[];
|
||||
};
|
@ -12,6 +12,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
@ -39,7 +40,6 @@ import {
|
||||
AuthTokenType,
|
||||
MfaMethod
|
||||
} from "./auth-type";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
|
||||
type TAuthLoginServiceFactoryDep = {
|
||||
userDAL: TUserDALFactory;
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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"
|
||||
|
@ -10,7 +10,8 @@ export enum SecretSync {
|
||||
TerraformCloud = "terraform-cloud",
|
||||
Camunda = "camunda",
|
||||
Vercel = "vercel",
|
||||
Windmill = "windmill"
|
||||
Windmill = "windmill",
|
||||
TeamCity = "teamcity"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
@ -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}`
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
4
backend/src/services/secret-sync/teamcity/index.ts
Normal file
4
backend/src/services/secret-sync/teamcity/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./teamcity-sync-constants";
|
||||
export * from "./teamcity-sync-fns";
|
||||
export * from "./teamcity-sync-schemas";
|
||||
export * from "./teamcity-sync-types";
|
@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const TEAMCITY_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "TeamCity",
|
||||
destination: SecretSync.TeamCity,
|
||||
connection: AppConnection.TeamCity,
|
||||
canImportSecrets: true
|
||||
};
|
183
backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts
Normal file
183
backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts
Normal file
@ -0,0 +1,183 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { getTeamCityInstanceUrl } from "@app/services/app-connection/teamcity";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
import {
|
||||
TDeleteTeamCityVariable,
|
||||
TPostTeamCityVariable,
|
||||
TTeamCityListVariables,
|
||||
TTeamCityListVariablesResponse,
|
||||
TTeamCitySyncWithCredentials
|
||||
} from "@app/services/secret-sync/teamcity/teamcity-sync-types";
|
||||
|
||||
// Note: Most variables won't be returned with a value due to them being a "password" type (starting with "env.").
|
||||
// TeamCity API returns empty string for password-type variables for security reasons.
|
||||
const listTeamCityVariables = async ({ instanceUrl, accessToken, project, buildConfig }: TTeamCityListVariables) => {
|
||||
const { data } = await request.get<TTeamCityListVariablesResponse>(
|
||||
buildConfig
|
||||
? `${instanceUrl}/app/rest/buildTypes/${encodeURIComponent(buildConfig)}/parameters`
|
||||
: `${instanceUrl}/app/rest/projects/id:${encodeURIComponent(project)}/parameters`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Strips out "env." from map key, but the "name" field still has the original unaltered key.
|
||||
return Object.fromEntries(
|
||||
data.property.map((variable) => [
|
||||
variable.name.startsWith("env.") ? variable.name.substring(4) : variable.name,
|
||||
{ ...variable, value: variable.value || "" } // Password values will be empty strings from the API for security
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
// Create and update both use the same method
|
||||
const updateTeamCityVariable = async ({
|
||||
instanceUrl,
|
||||
accessToken,
|
||||
project,
|
||||
buildConfig,
|
||||
key,
|
||||
value
|
||||
}: TPostTeamCityVariable) => {
|
||||
return request.post(
|
||||
buildConfig
|
||||
? `${instanceUrl}/app/rest/buildTypes/${encodeURIComponent(buildConfig)}/parameters`
|
||||
: `${instanceUrl}/app/rest/projects/id:${encodeURIComponent(project)}/parameters`,
|
||||
{
|
||||
name: key,
|
||||
value,
|
||||
type: {
|
||||
rawValue: "password display='hidden'"
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const deleteTeamCityVariable = async ({
|
||||
instanceUrl,
|
||||
accessToken,
|
||||
project,
|
||||
buildConfig,
|
||||
key
|
||||
}: TDeleteTeamCityVariable) => {
|
||||
return request.delete(
|
||||
buildConfig
|
||||
? `${instanceUrl}/app/rest/buildTypes/${encodeURIComponent(buildConfig)}/parameters/${encodeURIComponent(key)}`
|
||||
: `${instanceUrl}/app/rest/projects/id:${encodeURIComponent(project)}/parameters/${encodeURIComponent(key)}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const TeamCitySyncFns = {
|
||||
syncSecrets: async (secretSync: TTeamCitySyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { project, buildConfig }
|
||||
} = secretSync;
|
||||
|
||||
const instanceUrl = await getTeamCityInstanceUrl(connection);
|
||||
const { accessToken } = connection.credentials;
|
||||
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value }] = entry;
|
||||
|
||||
const payload = {
|
||||
instanceUrl,
|
||||
accessToken,
|
||||
project,
|
||||
buildConfig,
|
||||
key: `env.${key}`,
|
||||
value
|
||||
};
|
||||
|
||||
try {
|
||||
// Replace every secret since TeamCity does not return secret values that we can cross-check
|
||||
// No need to differenciate create / update because TeamCity uses the same method for both
|
||||
await updateTeamCityVariable(payload);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||
|
||||
const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig });
|
||||
|
||||
for await (const [key, variable] of Object.entries(variables)) {
|
||||
if (!(key in secretMap)) {
|
||||
try {
|
||||
await deleteTeamCityVariable({
|
||||
key: variable.name, // We use variable.name instead of key because key is stripped of "env." prefix in listTeamCityVariables().
|
||||
instanceUrl,
|
||||
accessToken,
|
||||
project,
|
||||
buildConfig
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
removeSecrets: async (secretSync: TTeamCitySyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { project, buildConfig }
|
||||
} = secretSync;
|
||||
|
||||
const instanceUrl = await getTeamCityInstanceUrl(connection);
|
||||
const { accessToken } = connection.credentials;
|
||||
|
||||
const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig });
|
||||
|
||||
for await (const [key, variable] of Object.entries(variables)) {
|
||||
if (key in secretMap) {
|
||||
try {
|
||||
await deleteTeamCityVariable({
|
||||
key: variable.name, // We use variable.name instead of key because key is stripped of "env." prefix in listTeamCityVariables().
|
||||
instanceUrl,
|
||||
accessToken,
|
||||
project,
|
||||
buildConfig
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getSecrets: async (secretSync: TTeamCitySyncWithCredentials) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { project, buildConfig }
|
||||
} = secretSync;
|
||||
|
||||
const instanceUrl = await getTeamCityInstanceUrl(connection);
|
||||
const { accessToken } = connection.credentials;
|
||||
|
||||
return listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig });
|
||||
}
|
||||
};
|
@ -0,0 +1,44 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const TeamCitySyncDestinationConfigSchema = z.object({
|
||||
project: z.string().trim().min(1, "Project required").describe(SecretSyncs.DESTINATION_CONFIG.TEAMCITY.project),
|
||||
buildConfig: z.string().trim().optional().describe(SecretSyncs.DESTINATION_CONFIG.TEAMCITY.buildConfig)
|
||||
});
|
||||
|
||||
const TeamCitySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const TeamCitySyncSchema = BaseSecretSyncSchema(SecretSync.TeamCity, TeamCitySyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.TeamCity),
|
||||
destinationConfig: TeamCitySyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateTeamCitySyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.TeamCity,
|
||||
TeamCitySyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: TeamCitySyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateTeamCitySyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.TeamCity,
|
||||
TeamCitySyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: TeamCitySyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const TeamCitySyncListItemSchema = z.object({
|
||||
name: z.literal("TeamCity"),
|
||||
connection: z.literal(AppConnection.TeamCity),
|
||||
destination: z.literal(SecretSync.TeamCity),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
@ -0,0 +1,46 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TTeamCityConnection } from "@app/services/app-connection/teamcity";
|
||||
|
||||
import { CreateTeamCitySyncSchema, TeamCitySyncListItemSchema, TeamCitySyncSchema } from "./teamcity-sync-schemas";
|
||||
|
||||
export type TTeamCitySync = z.infer<typeof TeamCitySyncSchema>;
|
||||
|
||||
export type TTeamCitySyncInput = z.infer<typeof CreateTeamCitySyncSchema>;
|
||||
|
||||
export type TTeamCitySyncListItem = z.infer<typeof TeamCitySyncListItemSchema>;
|
||||
|
||||
export type TTeamCitySyncWithCredentials = TTeamCitySync & {
|
||||
connection: TTeamCityConnection;
|
||||
};
|
||||
|
||||
export type TTeamCityVariable = {
|
||||
name: string;
|
||||
value: string;
|
||||
inherited?: boolean;
|
||||
type: {
|
||||
rawValue: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TTeamCityListVariablesResponse = {
|
||||
property: (TTeamCityVariable & { value?: string })[];
|
||||
count: number;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export type TTeamCityListVariables = {
|
||||
accessToken: string;
|
||||
instanceUrl: string;
|
||||
project: string;
|
||||
buildConfig?: string;
|
||||
};
|
||||
|
||||
export type TPostTeamCityVariable = TTeamCityListVariables & {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type TDeleteTeamCityVariable = TTeamCityListVariables & {
|
||||
key: string;
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/ldap/available"
|
||||
---
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/ldap"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [LDAP Connections](/integrations/app-connections/ldap) to learn how to obtain
|
||||
the required credentials.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/ldap/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/ldap/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/ldap/connection-name/{connectionName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/ldap"
|
||||
---
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/app-connections/ldap/{connectionId}"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [LDAP Connections](/integrations/app-connections/ldap) to learn how to obtain
|
||||
the required credentials.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/teamcity/available"
|
||||
---
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/teamcity"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [TeamCity Connections](/integrations/app-connections/teamcity) to learn how to obtain
|
||||
the required credentials.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/teamcity/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/teamcity/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/teamcity/connection-name/{connectionName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/teamcity"
|
||||
---
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/app-connections/teamcity/{connectionId}"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [TeamCity Connections](/integrations/app-connections/teamcity) to learn how to obtain
|
||||
the required credentials.
|
||||
</Note>
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v2/secret-rotations/aws-iam-user-secret"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [AWS IAM User Secret Rotations](/documentation/platform/secret-rotation/aws-iam-user-secret) to learn how to obtain the
|
||||
required parameters.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret/rotation-name/{rotationName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get Credentials by ID"
|
||||
openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}/generated-credentials"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v2/secret-rotations/aws-iam-user-secret"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Rotate Secrets"
|
||||
openapi: "POST /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}/rotate-secrets"
|
||||
---
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v2/secret-rotations/aws-iam-user-secret/{rotationId}"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [AWS IAM User Secret Rotations](/documentation/platform/secret-rotation/aws-iam-user-secret) to learn how to obtain the
|
||||
required parameters.
|
||||
</Note>
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v2/secret-rotations/ldap-password"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [LDAP Password Rotations](/documentation/platform/secret-rotation/ldap-password) to learn how to obtain the
|
||||
required parameters.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v2/secret-rotations/ldap-password/{rotationId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v2/secret-rotations/ldap-password/{rotationId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v2/secret-rotations/ldap-password/rotation-name/{rotationName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get Credentials by ID"
|
||||
openapi: "GET /api/v2/secret-rotations/ldap-password/{rotationId}/generated-credentials"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v2/secret-rotations/ldap-password"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Rotate Secrets"
|
||||
openapi: "POST /api/v2/secret-rotations/ldap-password/{rotationId}/rotate-secrets"
|
||||
---
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v2/secret-rotations/ldap-password/{rotationId}"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [LDAP Rotations](/documentation/platform/secret-rotation/ldap-password) to learn how to obtain the
|
||||
required parameters.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/secret-syncs/teamcity"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/secret-syncs/teamcity/{syncId}"
|
||||
---
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user