Compare commits
40 Commits
improve-in
...
server-adm
Author | SHA1 | Date | |
---|---|---|---|
|
77a8cd9efc | ||
|
7357d377e1 | ||
|
573b990aa3 | ||
|
e15086edc0 | ||
|
13ef3809bd | ||
|
fb49c9250a | ||
|
5ced7fa923 | ||
|
5ffd42378a | ||
|
f995708e44 | ||
|
c266d68993 | ||
|
c7c8107f85 | ||
|
b906fe34a1 | ||
|
bec1fefee8 | ||
|
cd03107a60 | ||
|
07965de1db | ||
|
b20ff0f029 | ||
|
691cbe0a4f | ||
|
0787128803 | ||
|
837158e344 | ||
|
03bd1471b2 | ||
|
f53c39f65b | ||
|
092695089d | ||
|
2d80681597 | ||
|
cf23f98170 | ||
|
c4c8e121f0 | ||
|
0701c996e5 | ||
|
4ca6f165b7 | ||
|
b9dd565926 | ||
|
136b0bdcb5 | ||
|
7266d1f310 | ||
|
9c6ec807cb | ||
|
5fcae35fae | ||
|
359e19f804 | ||
|
2aa548c7dc | ||
|
4f00fc6777 | ||
|
82b765553c | ||
|
8972521716 | ||
|
81b45b24ec | ||
|
f2b0e4ae37 | ||
|
b4ed1fa96a |
@@ -55,6 +55,8 @@ USER non-root-user
|
||||
##
|
||||
FROM base AS backend-build
|
||||
|
||||
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install all required dependencies for build
|
||||
@@ -84,6 +86,8 @@ RUN npm run build
|
||||
# Production stage
|
||||
FROM base AS backend-runner
|
||||
|
||||
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install all required dependencies for runtime
|
||||
@@ -112,6 +116,11 @@ RUN mkdir frontend-build
|
||||
FROM base AS production
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool \
|
||||
libssl-dev \
|
||||
ca-certificates \
|
||||
bash \
|
||||
curl \
|
||||
@@ -171,6 +180,7 @@ ENV NODE_ENV production
|
||||
ENV STANDALONE_BUILD true
|
||||
ENV STANDALONE_MODE true
|
||||
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
||||
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
|
55
backend/package-lock.json
generated
@@ -7,7 +7,6 @@
|
||||
"": {
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-elasticache": "^3.637.0",
|
||||
@@ -34,7 +33,7 @@
|
||||
"@gitbeaker/rest": "^42.5.0",
|
||||
"@google-cloud/kms": "^4.5.0",
|
||||
"@infisical/quic": "^1.0.8",
|
||||
"@node-saml/passport-saml": "^5.0.1",
|
||||
"@node-saml/passport-saml": "^5.1.0",
|
||||
"@octokit/auth-app": "^7.1.1",
|
||||
"@octokit/core": "^5.2.1",
|
||||
"@octokit/plugin-paginate-graphql": "^4.0.1",
|
||||
@@ -9574,20 +9573,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/node-saml": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-5.0.1.tgz",
|
||||
"integrity": "sha512-YQzFPEC+CnsfO9AFYnwfYZKIzOLx3kITaC1HrjHVLTo6hxcQhc+LgHODOMvW4VCV95Gwrz1MshRUWCPzkDqmnA==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-5.1.0.tgz",
|
||||
"integrity": "sha512-t3cJnZ4aC7HhPZ6MGylGZULvUtBOZ6FzuUndaHGXjmIZHXnLfC/7L8a57O9Q9V7AxJGKAiRM5zu2wNm9EsvQpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/qs": "^6.9.11",
|
||||
"@types/qs": "^6.9.18",
|
||||
"@types/xml-encryption": "^1.2.4",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"@xmldom/is-dom-node": "^1.0.1",
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"debug": "^4.3.4",
|
||||
"xml-crypto": "^6.0.1",
|
||||
"xml-encryption": "^3.0.2",
|
||||
"debug": "^4.4.0",
|
||||
"xml-crypto": "^6.1.2",
|
||||
"xml-encryption": "^3.1.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"xpath": "^0.0.34"
|
||||
@@ -9597,9 +9596,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/node-saml/node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -9636,14 +9635,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/passport-saml": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-5.0.1.tgz",
|
||||
"integrity": "sha512-fMztg3zfSnjLEgxvpl6HaDMNeh0xeQX4QHiF9e2Lsie2dc4qFE37XYbQZhVmn8XJ2awPpSWLQ736UskYgGU8lQ==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-5.1.0.tgz",
|
||||
"integrity": "sha512-pBm+iFjv9eihcgeJuSUs4c0AuX1QEFdHwP8w1iaWCfDzXdeWZxUBU5HT2bY2S4dvNutcy+A9hYsH7ZLBGtgwDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@node-saml/node-saml": "^5.0.1",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/passport": "^1.0.16",
|
||||
"@node-saml/node-saml": "^5.1.0",
|
||||
"@types/express": "^4.17.23",
|
||||
"@types/passport": "^1.0.17",
|
||||
"@types/passport-strategy": "^0.2.38",
|
||||
"passport": "^0.7.0",
|
||||
"passport-strategy": "^1.0.0"
|
||||
@@ -13351,9 +13350,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
||||
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz",
|
||||
"integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^4.17.33",
|
||||
@@ -13523,9 +13523,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/passport": {
|
||||
"version": "1.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz",
|
||||
"integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==",
|
||||
"version": "1.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
|
||||
"integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
@@ -31953,9 +31954,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml-crypto": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.0.1.tgz",
|
||||
"integrity": "sha512-v05aU7NS03z4jlZ0iZGRFeZsuKO1UfEbbYiaeRMiATBFs6Jq9+wqKquEMTn4UTrYZ9iGD8yz3KT4L9o2iF682w==",
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.1.2.tgz",
|
||||
"integrity": "sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xmldom/is-dom-node": "^1.0.1",
|
||||
|
@@ -153,7 +153,7 @@
|
||||
"@gitbeaker/rest": "^42.5.0",
|
||||
"@google-cloud/kms": "^4.5.0",
|
||||
"@infisical/quic": "^1.0.8",
|
||||
"@node-saml/passport-saml": "^5.0.1",
|
||||
"@node-saml/passport-saml": "^5.1.0",
|
||||
"@octokit/auth-app": "^7.1.1",
|
||||
"@octokit/core": "^5.2.1",
|
||||
"@octokit/plugin-paginate-graphql": "^4.0.1",
|
||||
|
@@ -0,0 +1,16 @@
|
||||
import { registerSecretScanningEndpoints } from "@app/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-endpoints";
|
||||
import {
|
||||
CreateGitLabDataSourceSchema,
|
||||
GitLabDataSourceSchema,
|
||||
UpdateGitLabDataSourceSchema
|
||||
} from "@app/ee/services/secret-scanning-v2/gitlab";
|
||||
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
|
||||
export const registerGitLabSecretScanningRouter = async (server: FastifyZodProvider) =>
|
||||
registerSecretScanningEndpoints({
|
||||
type: SecretScanningDataSource.GitLab,
|
||||
server,
|
||||
responseSchema: GitLabDataSourceSchema,
|
||||
createSchema: CreateGitLabDataSourceSchema,
|
||||
updateSchema: UpdateGitLabDataSourceSchema
|
||||
});
|
@@ -1,3 +1,4 @@
|
||||
import { registerGitLabSecretScanningRouter } from "@app/ee/routes/v2/secret-scanning-v2-routers/gitlab-secret-scanning-router";
|
||||
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
|
||||
import { registerBitbucketSecretScanningRouter } from "./bitbucket-secret-scanning-router";
|
||||
@@ -10,5 +11,6 @@ export const SECRET_SCANNING_REGISTER_ROUTER_MAP: Record<
|
||||
(server: FastifyZodProvider) => Promise<void>
|
||||
> = {
|
||||
[SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter,
|
||||
[SecretScanningDataSource.Bitbucket]: registerBitbucketSecretScanningRouter
|
||||
[SecretScanningDataSource.Bitbucket]: registerBitbucketSecretScanningRouter,
|
||||
[SecretScanningDataSource.GitLab]: registerGitLabSecretScanningRouter
|
||||
};
|
||||
|
@@ -4,6 +4,7 @@ import { SecretScanningConfigsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { BitbucketDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/bitbucket";
|
||||
import { GitHubDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/github";
|
||||
import { GitLabDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/gitlab";
|
||||
import {
|
||||
SecretScanningFindingStatus,
|
||||
SecretScanningScanStatus
|
||||
@@ -24,7 +25,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const SecretScanningDataSourceOptionsSchema = z.discriminatedUnion("type", [
|
||||
GitHubDataSourceListItemSchema,
|
||||
BitbucketDataSourceListItemSchema
|
||||
BitbucketDataSourceListItemSchema,
|
||||
GitLabDataSourceListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretScanningV2Router = async (server: FastifyZodProvider) => {
|
||||
|
@@ -5,13 +5,14 @@
|
||||
// TODO(akhilmhdh): With tony find out the api structure and fill it here
|
||||
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { AxiosError } from "axios";
|
||||
import { CronJob } from "cron";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { verifyOfflineLicense } from "@app/lib/crypto";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TIdentityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
@@ -603,10 +604,22 @@ export const licenseServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerCloudApi.request.delete(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods/${pmtMethodId}`
|
||||
);
|
||||
return data;
|
||||
try {
|
||||
const { data } = await licenseServerCloudApi.request.delete(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods/${pmtMethodId}`
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to remove payment method: ${error.response?.data?.message}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to remove payment method"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getOrgTaxIds = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgTaxIdDTO) => {
|
||||
|
@@ -65,7 +65,10 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns";
|
||||
import {
|
||||
hasSecretReadValueOrDescribePermission,
|
||||
throwIfMissingSecretReadValueOrDescribePermission
|
||||
} from "../permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
||||
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
|
||||
@@ -277,13 +280,19 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
) {
|
||||
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||
}
|
||||
|
||||
const hasSecretReadAccess = permission.can(
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
const getHasSecretReadAccess = (environment: string, tags: { slug: string }[], secretPath?: string) => {
|
||||
const canRead = hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||
environment,
|
||||
secretPath: secretPath || "/",
|
||||
secretTags: tags.map((i) => i.slug)
|
||||
});
|
||||
return canRead;
|
||||
};
|
||||
|
||||
let secrets;
|
||||
const secretPath = await folderDAL.findSecretPathByFolderIds(secretApprovalRequest.projectId, [
|
||||
secretApprovalRequest.folderId
|
||||
]);
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
@@ -299,8 +308,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
version: el.version,
|
||||
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
||||
secretValueHidden: !hasSecretReadAccess,
|
||||
secretValue: !hasSecretReadAccess
|
||||
secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path),
|
||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||
: el.secret && el.secret.isRotatedSecret
|
||||
? undefined
|
||||
@@ -315,8 +324,12 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretKey: el.secret.key,
|
||||
id: el.secret.id,
|
||||
version: el.secret.version,
|
||||
secretValueHidden: !hasSecretReadAccess,
|
||||
secretValue: !hasSecretReadAccess
|
||||
secretValueHidden: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
),
|
||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||
: el.secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
||||
@@ -331,8 +344,12 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretKey: el.secretVersion.key,
|
||||
id: el.secretVersion.id,
|
||||
version: el.secretVersion.version,
|
||||
secretValueHidden: !hasSecretReadAccess,
|
||||
secretValue: !hasSecretReadAccess
|
||||
secretValueHidden: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
),
|
||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||
: el.secretVersion.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
||||
@@ -350,7 +367,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
|
||||
secrets = encryptedSecrets.map((el) => ({
|
||||
...el,
|
||||
secretValueHidden: !hasSecretReadAccess,
|
||||
secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path),
|
||||
...decryptSecretWithBot(el, botKey),
|
||||
secret: el.secret
|
||||
? {
|
||||
@@ -370,9 +387,6 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
: undefined
|
||||
}));
|
||||
}
|
||||
const secretPath = await folderDAL.findSecretPathByFolderIds(secretApprovalRequest.projectId, [
|
||||
secretApprovalRequest.folderId
|
||||
]);
|
||||
|
||||
return { ...secretApprovalRequest, secretPath: secretPath?.[0]?.path || "/", commits: secrets };
|
||||
};
|
||||
|
@@ -21,6 +21,8 @@ const GRAPH_API_BASE = "https://graph.microsoft.com/v1.0";
|
||||
|
||||
type AzureErrorResponse = { error: { message: string } };
|
||||
|
||||
const EXPIRY_PADDING_IN_DAYS = 3;
|
||||
|
||||
const sleep = async () =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, 1000);
|
||||
@@ -33,7 +35,8 @@ export const azureClientSecretRotationFactory: TRotationFactory<
|
||||
const {
|
||||
connection,
|
||||
parameters: { objectId, clientId: clientIdParam },
|
||||
secretsMapping
|
||||
secretsMapping,
|
||||
rotationInterval
|
||||
} = secretRotation;
|
||||
|
||||
/**
|
||||
@@ -50,7 +53,7 @@ export const azureClientSecretRotationFactory: TRotationFactory<
|
||||
)}-${now.getFullYear()}`;
|
||||
|
||||
const endDateTime = new Date();
|
||||
endDateTime.setFullYear(now.getFullYear() + 5);
|
||||
endDateTime.setDate(now.getDate() + rotationInterval * 2 + EXPIRY_PADDING_IN_DAYS); // give 72 hour buffer
|
||||
|
||||
try {
|
||||
const { data } = await request.post<AzureAddPasswordResponse>(
|
||||
@@ -195,6 +198,12 @@ export const azureClientSecretRotationFactory: TRotationFactory<
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotateClientSecret();
|
||||
|
||||
// 2.5 years as expiry is set to x2 interval for the inactive period of credential
|
||||
if (rotationInterval > Math.floor(365 * 2.5) - EXPIRY_PADDING_IN_DAYS) {
|
||||
throw new BadRequestError({ message: "Azure does not support token duration over 5 years" });
|
||||
}
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
|
@@ -51,6 +51,7 @@ const baseSecretRotationV2Query = ({
|
||||
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
|
||||
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
|
||||
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
|
||||
db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"),
|
||||
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
|
||||
db
|
||||
@@ -104,6 +105,7 @@ const expandSecretRotation = <T extends Awaited<ReturnType<typeof baseSecretRota
|
||||
connectionCreatedAt,
|
||||
connectionUpdatedAt,
|
||||
connectionVersion,
|
||||
connectionGatewayId,
|
||||
connectionIsPlatformManagedCredentials,
|
||||
...el
|
||||
} = secretRotation;
|
||||
@@ -123,6 +125,7 @@ const expandSecretRotation = <T extends Awaited<ReturnType<typeof baseSecretRota
|
||||
createdAt: connectionCreatedAt,
|
||||
updatedAt: connectionUpdatedAt,
|
||||
version: connectionVersion,
|
||||
gatewayId: connectionGatewayId,
|
||||
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials
|
||||
},
|
||||
folder: {
|
||||
|
@@ -18,7 +18,8 @@ import {
|
||||
TSecretScanningFactoryInitialize,
|
||||
TSecretScanningFactoryListRawResources,
|
||||
TSecretScanningFactoryPostInitialization,
|
||||
TSecretScanningFactoryTeardown
|
||||
TSecretScanningFactoryTeardown,
|
||||
TSecretScanningFactoryValidateConfigUpdate
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
@@ -302,6 +303,13 @@ export const BitbucketSecretScanningFactory = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const validateConfigUpdate: TSecretScanningFactoryValidateConfigUpdate<
|
||||
TBitbucketDataSourceInput["config"],
|
||||
TBitbucketDataSourceWithConnection
|
||||
> = async () => {
|
||||
// no validation required
|
||||
};
|
||||
|
||||
return {
|
||||
initialize,
|
||||
postInitialization,
|
||||
@@ -309,6 +317,7 @@ export const BitbucketSecretScanningFactory = () => {
|
||||
getFullScanPath,
|
||||
getDiffScanResourcePayload,
|
||||
getDiffScanFindingsPayload,
|
||||
teardown
|
||||
teardown,
|
||||
validateConfigUpdate
|
||||
};
|
||||
};
|
||||
|
@@ -20,7 +20,8 @@ import {
|
||||
TSecretScanningFactoryInitialize,
|
||||
TSecretScanningFactoryListRawResources,
|
||||
TSecretScanningFactoryPostInitialization,
|
||||
TSecretScanningFactoryTeardown
|
||||
TSecretScanningFactoryTeardown,
|
||||
TSecretScanningFactoryValidateConfigUpdate
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
@@ -64,7 +65,14 @@ export const GitHubSecretScanningFactory = () => {
|
||||
};
|
||||
|
||||
const teardown: TSecretScanningFactoryTeardown<TGitHubDataSourceWithConnection> = async () => {
|
||||
// no termination required
|
||||
// no teardown required
|
||||
};
|
||||
|
||||
const validateConfigUpdate: TSecretScanningFactoryValidateConfigUpdate<
|
||||
TGitHubDataSourceInput["config"],
|
||||
TGitHubDataSourceWithConnection
|
||||
> = async () => {
|
||||
// no validation required
|
||||
};
|
||||
|
||||
const listRawResources: TSecretScanningFactoryListRawResources<TGitHubDataSourceWithConnection> = async (
|
||||
@@ -238,6 +246,7 @@ export const GitHubSecretScanningFactory = () => {
|
||||
getFullScanPath,
|
||||
getDiffScanResourcePayload,
|
||||
getDiffScanFindingsPayload,
|
||||
teardown
|
||||
teardown,
|
||||
validateConfigUpdate
|
||||
};
|
||||
};
|
||||
|
@@ -0,0 +1,9 @@
|
||||
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
import { TSecretScanningDataSourceListItem } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const GITLAB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION: TSecretScanningDataSourceListItem = {
|
||||
name: "GitLab",
|
||||
type: SecretScanningDataSource.GitLab,
|
||||
connection: AppConnection.GitLab
|
||||
};
|
@@ -0,0 +1,8 @@
|
||||
export enum GitLabDataSourceScope {
|
||||
Project = "project",
|
||||
Group = "group"
|
||||
}
|
||||
|
||||
export enum GitLabWebHookEvent {
|
||||
Push = "Push Hook"
|
||||
}
|
@@ -0,0 +1,409 @@
|
||||
import { Camelize, GitbeakerRequestError, GroupHookSchema, ProjectHookSchema } from "@gitbeaker/rest";
|
||||
import { join } from "path";
|
||||
|
||||
import { scanContentAndGetFindings } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
|
||||
import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
|
||||
import {
|
||||
SecretScanningFindingSeverity,
|
||||
SecretScanningResource
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
import {
|
||||
cloneRepository,
|
||||
convertPatchLineToFileLineNumber,
|
||||
replaceNonChangesWithNewlines
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns";
|
||||
import {
|
||||
TSecretScanningFactoryGetDiffScanFindingsPayload,
|
||||
TSecretScanningFactoryGetDiffScanResourcePayload,
|
||||
TSecretScanningFactoryGetFullScanPath,
|
||||
TSecretScanningFactoryInitialize,
|
||||
TSecretScanningFactoryListRawResources,
|
||||
TSecretScanningFactoryParams,
|
||||
TSecretScanningFactoryPostInitialization,
|
||||
TSecretScanningFactoryTeardown,
|
||||
TSecretScanningFactoryValidateConfigUpdate
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { titleCaseToCamelCase } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { GitLabProjectRegex } from "@app/lib/regex";
|
||||
import {
|
||||
getGitLabConnectionClient,
|
||||
getGitLabInstanceUrl,
|
||||
TGitLabConnection
|
||||
} from "@app/services/app-connection/gitlab";
|
||||
|
||||
import { GitLabDataSourceScope } from "./gitlab-secret-scanning-enums";
|
||||
import {
|
||||
TGitLabDataSourceCredentials,
|
||||
TGitLabDataSourceInput,
|
||||
TGitLabDataSourceWithConnection,
|
||||
TQueueGitLabResourceDiffScan
|
||||
} from "./gitlab-secret-scanning-types";
|
||||
|
||||
const getMainDomain = (instanceUrl: string) => {
|
||||
const url = new URL(instanceUrl);
|
||||
const { hostname } = url;
|
||||
const parts = hostname.split(".");
|
||||
|
||||
if (parts.length >= 2) {
|
||||
return parts.slice(-2).join(".");
|
||||
}
|
||||
|
||||
return hostname;
|
||||
};
|
||||
|
||||
export const GitLabSecretScanningFactory = ({ appConnectionDAL, kmsService }: TSecretScanningFactoryParams) => {
|
||||
const initialize: TSecretScanningFactoryInitialize<
|
||||
TGitLabDataSourceInput,
|
||||
TGitLabConnection,
|
||||
TGitLabDataSourceCredentials
|
||||
> = async ({ payload: { config, name }, connection }, callback) => {
|
||||
const token = alphaNumericNanoId(64);
|
||||
|
||||
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (config.scope === GitLabDataSourceScope.Project) {
|
||||
const { projectId } = config;
|
||||
const project = await client.Projects.show(projectId);
|
||||
|
||||
if (!project) {
|
||||
throw new BadRequestError({ message: `Could not find project with ID ${projectId}.` });
|
||||
}
|
||||
|
||||
let hook: Camelize<ProjectHookSchema>;
|
||||
try {
|
||||
hook = await client.ProjectHooks.add(projectId, `${appCfg.SITE_URL}/secret-scanning/webhooks/gitlab`, {
|
||||
token,
|
||||
pushEvents: true,
|
||||
enableSslVerification: true,
|
||||
// @ts-expect-error gitbeaker is outdated, and the types don't support this field yet
|
||||
name: `Infisical Secret Scanning - ${name}`
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof GitbeakerRequestError) {
|
||||
throw new BadRequestError({ message: `${error.message}: ${error.cause?.description ?? "Unknown Error"}` });
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
return await callback({
|
||||
credentials: {
|
||||
token,
|
||||
hookId: hook.id
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
await client.ProjectHooks.remove(projectId, hook.id);
|
||||
} catch {
|
||||
// do nothing, just try to clean up webhook
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// group scope
|
||||
const { groupId } = config;
|
||||
|
||||
const group = await client.Groups.show(groupId);
|
||||
|
||||
if (!group) {
|
||||
throw new BadRequestError({ message: `Could not find group with ID ${groupId}.` });
|
||||
}
|
||||
|
||||
let hook: Camelize<GroupHookSchema>;
|
||||
try {
|
||||
hook = await client.GroupHooks.add(groupId, `${appCfg.SITE_URL}/secret-scanning/webhooks/gitlab`, {
|
||||
token,
|
||||
pushEvents: true,
|
||||
enableSslVerification: true,
|
||||
// @ts-expect-error gitbeaker is outdated, and the types don't support this field yet
|
||||
name: `Infisical Secret Scanning - ${name}`
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof GitbeakerRequestError) {
|
||||
throw new BadRequestError({ message: `${error.message}: ${error.cause?.description ?? "Unknown Error"}` });
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
return await callback({
|
||||
credentials: {
|
||||
token,
|
||||
hookId: hook.id
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
await client.GroupHooks.remove(groupId, hook.id);
|
||||
} catch {
|
||||
// do nothing, just try to clean up webhook
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const postInitialization: TSecretScanningFactoryPostInitialization<
|
||||
TGitLabDataSourceInput,
|
||||
TGitLabConnection,
|
||||
TGitLabDataSourceCredentials
|
||||
> = async ({ connection, dataSourceId, credentials, payload: { config } }) => {
|
||||
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
|
||||
const appCfg = getConfig();
|
||||
|
||||
const hookUrl = `${appCfg.SITE_URL}/secret-scanning/webhooks/gitlab`;
|
||||
const { hookId } = credentials;
|
||||
|
||||
if (config.scope === GitLabDataSourceScope.Project) {
|
||||
const { projectId } = config;
|
||||
|
||||
try {
|
||||
await client.ProjectHooks.edit(projectId, hookId, hookUrl, {
|
||||
// @ts-expect-error gitbeaker is outdated, and the types don't support this field yet
|
||||
name: `Infisical Secret Scanning - ${dataSourceId}`,
|
||||
custom_headers: [{ key: "x-data-source-id", value: dataSourceId }]
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
await client.ProjectHooks.remove(projectId, hookId);
|
||||
} catch {
|
||||
// do nothing, just try to clean up webhook
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// group-scope
|
||||
const { groupId } = config;
|
||||
|
||||
try {
|
||||
await client.GroupHooks.edit(groupId, hookId, hookUrl, {
|
||||
// @ts-expect-error gitbeaker is outdated, and the types don't support this field yet
|
||||
name: `Infisical Secret Scanning - ${dataSourceId}`,
|
||||
custom_headers: [{ key: "x-data-source-id", value: dataSourceId }]
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
await client.GroupHooks.remove(groupId, hookId);
|
||||
} catch {
|
||||
// do nothing, just try to clean up webhook
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const listRawResources: TSecretScanningFactoryListRawResources<TGitLabDataSourceWithConnection> = async (
|
||||
dataSource
|
||||
) => {
|
||||
const { connection, config } = dataSource;
|
||||
|
||||
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
|
||||
|
||||
if (config.scope === GitLabDataSourceScope.Project) {
|
||||
const { projectId } = config;
|
||||
|
||||
const project = await client.Projects.show(projectId);
|
||||
|
||||
if (!project) {
|
||||
throw new BadRequestError({ message: `Could not find project with ID ${projectId}.` });
|
||||
}
|
||||
|
||||
// scott: even though we have this data we want to get potentially updated name
|
||||
return [
|
||||
{
|
||||
name: project.pathWithNamespace,
|
||||
externalId: project.id.toString(),
|
||||
type: SecretScanningResource.Project
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// group-scope
|
||||
|
||||
const { groupId, includeProjects } = config;
|
||||
|
||||
const projects = await client.Groups.allProjects(groupId, {
|
||||
archived: false
|
||||
});
|
||||
|
||||
const filteredProjects: typeof projects = [];
|
||||
if (!includeProjects || includeProjects.includes("*")) {
|
||||
filteredProjects.push(...projects);
|
||||
} else {
|
||||
filteredProjects.push(...projects.filter((project) => includeProjects.includes(project.pathWithNamespace)));
|
||||
}
|
||||
|
||||
return filteredProjects.map(({ id, pathWithNamespace }) => ({
|
||||
name: pathWithNamespace,
|
||||
externalId: id.toString(),
|
||||
type: SecretScanningResource.Project
|
||||
}));
|
||||
};
|
||||
|
||||
const getFullScanPath: TSecretScanningFactoryGetFullScanPath<TGitLabDataSourceWithConnection> = async ({
|
||||
dataSource,
|
||||
resourceName,
|
||||
tempFolder
|
||||
}) => {
|
||||
const { connection } = dataSource;
|
||||
|
||||
const instanceUrl = await getGitLabInstanceUrl(connection.credentials.instanceUrl);
|
||||
|
||||
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
|
||||
|
||||
const user = await client.Users.showCurrentUser();
|
||||
|
||||
const repoPath = join(tempFolder, "repo.git");
|
||||
|
||||
if (!GitLabProjectRegex.test(resourceName)) {
|
||||
throw new Error("Invalid GitLab project name");
|
||||
}
|
||||
|
||||
await cloneRepository({
|
||||
cloneUrl: `https://${user.username}:${connection.credentials.accessToken}@${getMainDomain(instanceUrl)}/${resourceName}.git`,
|
||||
repoPath
|
||||
});
|
||||
|
||||
return repoPath;
|
||||
};
|
||||
|
||||
const teardown: TSecretScanningFactoryTeardown<
|
||||
TGitLabDataSourceWithConnection,
|
||||
TGitLabDataSourceCredentials
|
||||
> = async ({ dataSource: { connection, config }, credentials: { hookId } }) => {
|
||||
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
|
||||
|
||||
if (config.scope === GitLabDataSourceScope.Project) {
|
||||
const { projectId } = config;
|
||||
try {
|
||||
await client.ProjectHooks.remove(projectId, hookId);
|
||||
} catch (error) {
|
||||
// do nothing, just try to clean up webhook
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { groupId } = config;
|
||||
try {
|
||||
await client.GroupHooks.remove(groupId, hookId);
|
||||
} catch (error) {
|
||||
// do nothing, just try to clean up webhook
|
||||
}
|
||||
};
|
||||
|
||||
const getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload<
|
||||
TQueueGitLabResourceDiffScan["payload"]
|
||||
> = ({ project }) => {
|
||||
return {
|
||||
name: project.path_with_namespace,
|
||||
externalId: project.id.toString(),
|
||||
type: SecretScanningResource.Project
|
||||
};
|
||||
};
|
||||
|
||||
const getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<
|
||||
TGitLabDataSourceWithConnection,
|
||||
TQueueGitLabResourceDiffScan["payload"]
|
||||
> = async ({ dataSource, payload, resourceName, configPath }) => {
|
||||
const { connection } = dataSource;
|
||||
|
||||
const client = await getGitLabConnectionClient(connection, appConnectionDAL, kmsService);
|
||||
|
||||
const { commits, project } = payload;
|
||||
|
||||
const allFindings: SecretMatch[] = [];
|
||||
|
||||
for (const commit of commits) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const commitDiffs = await client.Commits.showDiff(project.id, commit.id);
|
||||
|
||||
for (const commitDiff of commitDiffs) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (commitDiff.deletedFile) continue;
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const findings = await scanContentAndGetFindings(
|
||||
replaceNonChangesWithNewlines(`\n${commitDiff.diff}`),
|
||||
configPath
|
||||
);
|
||||
|
||||
const adjustedFindings = findings.map((finding) => {
|
||||
const startLine = convertPatchLineToFileLineNumber(commitDiff.diff, finding.StartLine);
|
||||
const endLine =
|
||||
finding.StartLine === finding.EndLine
|
||||
? startLine
|
||||
: convertPatchLineToFileLineNumber(commitDiff.diff, finding.EndLine);
|
||||
const startColumn = finding.StartColumn - 1; // subtract 1 for +
|
||||
const endColumn = finding.EndColumn - 1; // subtract 1 for +
|
||||
const authorName = commit.author.name;
|
||||
const authorEmail = commit.author.email;
|
||||
|
||||
return {
|
||||
...finding,
|
||||
StartLine: startLine,
|
||||
EndLine: endLine,
|
||||
StartColumn: startColumn,
|
||||
EndColumn: endColumn,
|
||||
File: commitDiff.newPath,
|
||||
Commit: commit.id,
|
||||
Author: authorName,
|
||||
Email: authorEmail,
|
||||
Message: commit.message,
|
||||
Fingerprint: `${commit.id}:${commitDiff.newPath}:${finding.RuleID}:${startLine}:${startColumn}`,
|
||||
Date: commit.timestamp,
|
||||
Link: `https://gitlab.com/${resourceName}/blob/${commit.id}/${commitDiff.newPath}#L${startLine}`
|
||||
};
|
||||
});
|
||||
|
||||
allFindings.push(...adjustedFindings);
|
||||
}
|
||||
}
|
||||
|
||||
return allFindings.map(
|
||||
({
|
||||
// discard match and secret as we don't want to store
|
||||
Match,
|
||||
Secret,
|
||||
...finding
|
||||
}) => ({
|
||||
details: titleCaseToCamelCase(finding),
|
||||
fingerprint: finding.Fingerprint,
|
||||
severity: SecretScanningFindingSeverity.High,
|
||||
rule: finding.RuleID
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const validateConfigUpdate: TSecretScanningFactoryValidateConfigUpdate<
|
||||
TGitLabDataSourceInput["config"],
|
||||
TGitLabDataSourceWithConnection
|
||||
> = async ({ config, dataSource }) => {
|
||||
if (dataSource.config.scope !== config.scope) {
|
||||
throw new BadRequestError({ message: "Cannot change Data Source scope after creation." });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listRawResources,
|
||||
getFullScanPath,
|
||||
initialize,
|
||||
postInitialization,
|
||||
teardown,
|
||||
getDiffScanResourcePayload,
|
||||
getDiffScanFindingsPayload,
|
||||
validateConfigUpdate
|
||||
};
|
||||
};
|
@@ -0,0 +1,101 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { GitLabDataSourceScope } from "@app/ee/services/secret-scanning-v2/gitlab/gitlab-secret-scanning-enums";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
SecretScanningResource
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
import {
|
||||
BaseCreateSecretScanningDataSourceSchema,
|
||||
BaseSecretScanningDataSourceSchema,
|
||||
BaseSecretScanningFindingSchema,
|
||||
BaseUpdateSecretScanningDataSourceSchema,
|
||||
GitRepositoryScanFindingDetailsSchema
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-schemas";
|
||||
import { SecretScanningDataSources } from "@app/lib/api-docs";
|
||||
import { GitLabProjectRegex } from "@app/lib/regex";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const GitLabDataSourceConfigSchema = z.discriminatedUnion("scope", [
|
||||
z.object({
|
||||
scope: z.literal(GitLabDataSourceScope.Group).describe(SecretScanningDataSources.CONFIG.GITLAB.scope),
|
||||
groupId: z.number().describe(SecretScanningDataSources.CONFIG.GITLAB.groupId),
|
||||
groupName: z.string().trim().max(256).optional().describe(SecretScanningDataSources.CONFIG.GITLAB.groupName),
|
||||
includeProjects: z
|
||||
.array(
|
||||
z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(256)
|
||||
.refine((value) => value === "*" || GitLabProjectRegex.test(value), "Invalid project name format")
|
||||
)
|
||||
.nonempty("One or more projects required")
|
||||
.max(100, "Cannot configure more than 100 projects")
|
||||
.default(["*"])
|
||||
.describe(SecretScanningDataSources.CONFIG.GITLAB.includeProjects)
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GitLabDataSourceScope.Project).describe(SecretScanningDataSources.CONFIG.GITLAB.scope),
|
||||
projectName: z.string().trim().max(256).optional().describe(SecretScanningDataSources.CONFIG.GITLAB.projectName),
|
||||
projectId: z.number().describe(SecretScanningDataSources.CONFIG.GITLAB.projectId)
|
||||
})
|
||||
]);
|
||||
|
||||
export const GitLabDataSourceSchema = BaseSecretScanningDataSourceSchema({
|
||||
type: SecretScanningDataSource.GitLab,
|
||||
isConnectionRequired: true
|
||||
})
|
||||
.extend({
|
||||
config: GitLabDataSourceConfigSchema
|
||||
})
|
||||
.describe(
|
||||
JSON.stringify({
|
||||
title: "GitLab"
|
||||
})
|
||||
);
|
||||
|
||||
export const CreateGitLabDataSourceSchema = BaseCreateSecretScanningDataSourceSchema({
|
||||
type: SecretScanningDataSource.GitLab,
|
||||
isConnectionRequired: true
|
||||
})
|
||||
.extend({
|
||||
config: GitLabDataSourceConfigSchema
|
||||
})
|
||||
.describe(
|
||||
JSON.stringify({
|
||||
title: "GitLab"
|
||||
})
|
||||
);
|
||||
|
||||
export const UpdateGitLabDataSourceSchema = BaseUpdateSecretScanningDataSourceSchema(SecretScanningDataSource.GitLab)
|
||||
.extend({
|
||||
config: GitLabDataSourceConfigSchema.optional()
|
||||
})
|
||||
.describe(
|
||||
JSON.stringify({
|
||||
title: "GitLab"
|
||||
})
|
||||
);
|
||||
|
||||
export const GitLabDataSourceListItemSchema = z
|
||||
.object({
|
||||
name: z.literal("GitLab"),
|
||||
connection: z.literal(AppConnection.GitLab),
|
||||
type: z.literal(SecretScanningDataSource.GitLab)
|
||||
})
|
||||
.describe(
|
||||
JSON.stringify({
|
||||
title: "GitLab"
|
||||
})
|
||||
);
|
||||
|
||||
export const GitLabFindingSchema = BaseSecretScanningFindingSchema.extend({
|
||||
resourceType: z.literal(SecretScanningResource.Project),
|
||||
dataSourceType: z.literal(SecretScanningDataSource.GitLab),
|
||||
details: GitRepositoryScanFindingDetailsSchema
|
||||
});
|
||||
|
||||
export const GitLabDataSourceCredentialsSchema = z.object({
|
||||
token: z.string(),
|
||||
hookId: z.number()
|
||||
});
|
@@ -0,0 +1,94 @@
|
||||
import { GitLabDataSourceScope } from "@app/ee/services/secret-scanning-v2/gitlab/gitlab-secret-scanning-enums";
|
||||
import { TSecretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal";
|
||||
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
import { TSecretScanningV2QueueServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-queue";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import {
|
||||
TGitLabDataSource,
|
||||
TGitLabDataSourceCredentials,
|
||||
THandleGitLabPushEvent
|
||||
} from "./gitlab-secret-scanning-types";
|
||||
|
||||
export const gitlabSecretScanningService = (
|
||||
secretScanningV2DAL: TSecretScanningV2DALFactory,
|
||||
secretScanningV2Queue: Pick<TSecretScanningV2QueueServiceFactory, "queueResourceDiffScan">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const handlePushEvent = async ({ payload, token, dataSourceId }: THandleGitLabPushEvent) => {
|
||||
if (!payload.total_commits_count || !payload.project) {
|
||||
logger.warn(
|
||||
`secretScanningV2PushEvent: GitLab - Insufficient data [changes=${
|
||||
payload.total_commits_count ?? 0
|
||||
}] [projectName=${payload.project?.path_with_namespace ?? "unknown"}] [projectId=${payload.project?.id ?? "unknown"}]`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataSource = (await secretScanningV2DAL.dataSources.findOne({
|
||||
id: dataSourceId,
|
||||
type: SecretScanningDataSource.GitLab
|
||||
})) as TGitLabDataSource | undefined;
|
||||
|
||||
if (!dataSource) {
|
||||
logger.error(
|
||||
`secretScanningV2PushEvent: GitLab - Could not find data source [dataSourceId=${dataSourceId}] [projectId=${payload.project.id}]`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { isAutoScanEnabled, config, encryptedCredentials, projectId } = dataSource;
|
||||
|
||||
if (!encryptedCredentials) {
|
||||
logger.info(
|
||||
`secretScanningV2PushEvent: GitLab - Could not find encrypted credentials [dataSourceId=${dataSource.id}] [projectId=${payload.project.id}]`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const decryptedCredentials = decryptor({ cipherTextBlob: encryptedCredentials });
|
||||
|
||||
const credentials = JSON.parse(decryptedCredentials.toString()) as TGitLabDataSourceCredentials;
|
||||
|
||||
if (token !== credentials.token) {
|
||||
logger.error(
|
||||
`secretScanningV2PushEvent: GitLab - Invalid webhook token [dataSourceId=${dataSource.id}] [projectId=${payload.project.id}]`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAutoScanEnabled) {
|
||||
logger.info(
|
||||
`secretScanningV2PushEvent: GitLab - ignoring due to auto scan disabled [dataSourceId=${dataSource.id}] [projectId=${payload.project.id}]`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
config.scope === GitLabDataSourceScope.Project
|
||||
? config.projectId.toString() === payload.project_id.toString()
|
||||
: config.includeProjects.includes("*") || config.includeProjects.includes(payload.project.path_with_namespace)
|
||||
) {
|
||||
await secretScanningV2Queue.queueResourceDiffScan({
|
||||
dataSourceType: SecretScanningDataSource.GitLab,
|
||||
payload,
|
||||
dataSourceId: dataSource.id
|
||||
});
|
||||
} else {
|
||||
logger.info(
|
||||
`secretScanningV2PushEvent: GitLab - ignoring due to repository not being present in config [dataSourceId=${dataSource.id}] [projectId=${payload.project.id}]`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handlePushEvent
|
||||
};
|
||||
};
|
@@ -0,0 +1,97 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
import { TGitLabConnection } from "@app/services/app-connection/gitlab";
|
||||
|
||||
import {
|
||||
CreateGitLabDataSourceSchema,
|
||||
GitLabDataSourceCredentialsSchema,
|
||||
GitLabDataSourceListItemSchema,
|
||||
GitLabDataSourceSchema,
|
||||
GitLabFindingSchema
|
||||
} from "./gitlab-secret-scanning-schemas";
|
||||
|
||||
export type TGitLabDataSource = z.infer<typeof GitLabDataSourceSchema>;
|
||||
|
||||
export type TGitLabDataSourceInput = z.infer<typeof CreateGitLabDataSourceSchema>;
|
||||
|
||||
export type TGitLabDataSourceListItem = z.infer<typeof GitLabDataSourceListItemSchema>;
|
||||
|
||||
export type TGitLabFinding = z.infer<typeof GitLabFindingSchema>;
|
||||
|
||||
export type TGitLabDataSourceWithConnection = TGitLabDataSource & {
|
||||
connection: TGitLabConnection;
|
||||
};
|
||||
|
||||
export type TGitLabDataSourceCredentials = z.infer<typeof GitLabDataSourceCredentialsSchema>;
|
||||
|
||||
export type TGitLabDataSourcePushEventPayload = {
|
||||
object_kind: "push";
|
||||
event_name: "push";
|
||||
before: string;
|
||||
after: string;
|
||||
ref: string;
|
||||
ref_protected: boolean;
|
||||
checkout_sha: string;
|
||||
user_id: number;
|
||||
user_name: string;
|
||||
user_username: string;
|
||||
user_email: string;
|
||||
user_avatar: string;
|
||||
project_id: number;
|
||||
project: {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
web_url: string;
|
||||
avatar_url: string | null;
|
||||
git_ssh_url: string;
|
||||
git_http_url: string;
|
||||
namespace: string;
|
||||
visibility_level: number;
|
||||
path_with_namespace: string;
|
||||
default_branch: string;
|
||||
homepage: string;
|
||||
url: string;
|
||||
ssh_url: string;
|
||||
http_url: string;
|
||||
};
|
||||
repository: {
|
||||
name: string;
|
||||
url: string;
|
||||
description: string;
|
||||
homepage: string;
|
||||
git_http_url: string;
|
||||
git_ssh_url: string;
|
||||
visibility_level: number;
|
||||
};
|
||||
commits: {
|
||||
id: string;
|
||||
message: string;
|
||||
title: string;
|
||||
timestamp: string;
|
||||
url: string;
|
||||
author: {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
added: string[];
|
||||
modified: string[];
|
||||
removed: string[];
|
||||
}[];
|
||||
total_commits_count: number;
|
||||
};
|
||||
|
||||
export type THandleGitLabPushEvent = {
|
||||
payload: TGitLabDataSourcePushEventPayload;
|
||||
dataSourceId: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type TQueueGitLabResourceDiffScan = {
|
||||
dataSourceType: SecretScanningDataSource.GitLab;
|
||||
payload: TGitLabDataSourcePushEventPayload;
|
||||
dataSourceId: string;
|
||||
resourceId: string;
|
||||
scanId: string;
|
||||
};
|
@@ -0,0 +1,3 @@
|
||||
export * from "./gitlab-secret-scanning-constants";
|
||||
export * from "./gitlab-secret-scanning-schemas";
|
||||
export * from "./gitlab-secret-scanning-types";
|
@@ -1,6 +1,7 @@
|
||||
export enum SecretScanningDataSource {
|
||||
GitHub = "github",
|
||||
Bitbucket = "bitbucket"
|
||||
Bitbucket = "bitbucket",
|
||||
GitLab = "gitlab"
|
||||
}
|
||||
|
||||
export enum SecretScanningScanStatus {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { BitbucketSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory";
|
||||
import { GitHubSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/github/github-secret-scanning-factory";
|
||||
import { GitLabSecretScanningFactory } from "@app/ee/services/secret-scanning-v2/gitlab/gitlab-secret-scanning-factory";
|
||||
|
||||
import { SecretScanningDataSource } from "./secret-scanning-v2-enums";
|
||||
import {
|
||||
@@ -19,5 +20,6 @@ type TSecretScanningFactoryImplementation = TSecretScanningFactory<
|
||||
|
||||
export const SECRET_SCANNING_FACTORY_MAP: Record<SecretScanningDataSource, TSecretScanningFactoryImplementation> = {
|
||||
[SecretScanningDataSource.GitHub]: GitHubSecretScanningFactory as TSecretScanningFactoryImplementation,
|
||||
[SecretScanningDataSource.Bitbucket]: BitbucketSecretScanningFactory as TSecretScanningFactoryImplementation
|
||||
[SecretScanningDataSource.Bitbucket]: BitbucketSecretScanningFactory as TSecretScanningFactoryImplementation,
|
||||
[SecretScanningDataSource.GitLab]: GitLabSecretScanningFactory as TSecretScanningFactoryImplementation
|
||||
};
|
||||
|
@@ -13,6 +13,7 @@ import {
|
||||
import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
|
||||
import { BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/bitbucket";
|
||||
import { GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/github";
|
||||
import { GITLAB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/gitlab";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
@@ -23,7 +24,8 @@ import { TCloneRepository, TGetFindingsPayload, TSecretScanningDataSourceListIte
|
||||
|
||||
const SECRET_SCANNING_SOURCE_LIST_OPTIONS: Record<SecretScanningDataSource, TSecretScanningDataSourceListItem> = {
|
||||
[SecretScanningDataSource.GitHub]: GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION,
|
||||
[SecretScanningDataSource.Bitbucket]: BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION
|
||||
[SecretScanningDataSource.Bitbucket]: BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION,
|
||||
[SecretScanningDataSource.GitLab]: GITLAB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretScanningDataSourceOptions = () => {
|
||||
|
@@ -3,15 +3,18 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
||||
|
||||
export const SECRET_SCANNING_DATA_SOURCE_NAME_MAP: Record<SecretScanningDataSource, string> = {
|
||||
[SecretScanningDataSource.GitHub]: "GitHub",
|
||||
[SecretScanningDataSource.Bitbucket]: "Bitbucket"
|
||||
[SecretScanningDataSource.Bitbucket]: "Bitbucket",
|
||||
[SecretScanningDataSource.GitLab]: "GitLab"
|
||||
};
|
||||
|
||||
export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<SecretScanningDataSource, AppConnection> = {
|
||||
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar,
|
||||
[SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket
|
||||
[SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket,
|
||||
[SecretScanningDataSource.GitLab]: AppConnection.GitLab
|
||||
};
|
||||
|
||||
export const AUTO_SYNC_DESCRIPTION_HELPER: Record<SecretScanningDataSource, { verb: string; noun: string }> = {
|
||||
[SecretScanningDataSource.GitHub]: { verb: "push", noun: "repositories" },
|
||||
[SecretScanningDataSource.Bitbucket]: { verb: "push", noun: "repositories" }
|
||||
[SecretScanningDataSource.Bitbucket]: { verb: "push", noun: "repositories" },
|
||||
[SecretScanningDataSource.GitLab]: { verb: "push", noun: "projects" }
|
||||
};
|
||||
|
@@ -16,6 +16,7 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
||||
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
@@ -48,6 +49,7 @@ type TSecretRotationV2QueueServiceFactoryDep = {
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">;
|
||||
};
|
||||
@@ -62,7 +64,8 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
smtpService,
|
||||
kmsService,
|
||||
auditLogService,
|
||||
keyStore
|
||||
keyStore,
|
||||
appConnectionDAL
|
||||
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
||||
const queueDataSourceFullScan = async (
|
||||
dataSource: TSecretScanningDataSourceWithConnection,
|
||||
@@ -71,7 +74,10 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
try {
|
||||
const { type } = dataSource;
|
||||
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[type]();
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[type]({
|
||||
kmsService,
|
||||
appConnectionDAL
|
||||
});
|
||||
|
||||
const rawResources = await factory.listRawResources(dataSource);
|
||||
|
||||
@@ -171,7 +177,10 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
let connection: TAppConnection | null = null;
|
||||
if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
|
||||
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]();
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]({
|
||||
kmsService,
|
||||
appConnectionDAL
|
||||
});
|
||||
|
||||
const findingsPath = join(tempFolder, "findings.json");
|
||||
|
||||
@@ -329,7 +338,10 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
dataSourceId,
|
||||
dataSourceType
|
||||
}: Pick<TQueueSecretScanningResourceDiffScan, "payload" | "dataSourceId" | "dataSourceType">) => {
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[dataSourceType as SecretScanningDataSource]();
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[dataSourceType as SecretScanningDataSource]({
|
||||
kmsService,
|
||||
appConnectionDAL
|
||||
});
|
||||
|
||||
const resourcePayload = factory.getDiffScanResourcePayload(payload);
|
||||
|
||||
@@ -391,7 +403,10 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
|
||||
if (!resource) throw new Error(`Resource with ID "${resourceId}" not found`);
|
||||
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]();
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]({
|
||||
kmsService,
|
||||
appConnectionDAL
|
||||
});
|
||||
|
||||
const tempFolder = await createTempFolder();
|
||||
|
||||
|
@@ -46,6 +46,7 @@ import {
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
|
||||
@@ -53,12 +54,14 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { bitbucketSecretScanningService } from "./bitbucket/bitbucket-secret-scanning-service";
|
||||
import { gitlabSecretScanningService } from "./gitlab/gitlab-secret-scanning-service";
|
||||
import { TSecretScanningV2DALFactory } from "./secret-scanning-v2-dal";
|
||||
import { TSecretScanningV2QueueServiceFactory } from "./secret-scanning-v2-queue";
|
||||
|
||||
export type TSecretScanningV2ServiceFactoryDep = {
|
||||
secretScanningV2DAL: TSecretScanningV2DALFactory;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
secretScanningV2Queue: Pick<
|
||||
@@ -76,6 +79,7 @@ export const secretScanningV2ServiceFactory = ({
|
||||
appConnectionService,
|
||||
licenseService,
|
||||
secretScanningV2Queue,
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}: TSecretScanningV2ServiceFactoryDep) => {
|
||||
const $checkListSecretScanningDataSourcesByProjectIdPermissions = async (
|
||||
@@ -255,7 +259,10 @@ export const secretScanningV2ServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[payload.type]();
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[payload.type]({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
try {
|
||||
const createdDataSource = await factory.initialize(
|
||||
@@ -363,6 +370,31 @@ export const secretScanningV2ServiceFactory = ({
|
||||
message: `Secret Scanning Data Source with ID "${dataSourceId}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}`
|
||||
});
|
||||
|
||||
let connection: TAppConnection | null = null;
|
||||
if (dataSource.connectionId) {
|
||||
// validates permission to connect and app is valid for data source
|
||||
connection = await appConnectionService.connectAppConnectionById(
|
||||
SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSource.type],
|
||||
dataSource.connectionId,
|
||||
actor
|
||||
);
|
||||
}
|
||||
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type]({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
if (payload.config) {
|
||||
await factory.validateConfigUpdate({
|
||||
dataSource: {
|
||||
...dataSource,
|
||||
connection
|
||||
} as TSecretScanningDataSourceWithConnection,
|
||||
config: payload.config as TSecretScanningDataSourceWithConnection["config"]
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedDataSource = await secretScanningV2DAL.dataSources.updateById(dataSourceId, payload);
|
||||
|
||||
@@ -416,7 +448,10 @@ export const secretScanningV2ServiceFactory = ({
|
||||
message: `Secret Scanning Data Source with ID "${dataSourceId}" is not configured for ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]}`
|
||||
});
|
||||
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[type]();
|
||||
const factory = SECRET_SCANNING_FACTORY_MAP[type]({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
let connection: TAppConnection | null = null;
|
||||
if (dataSource.connection) {
|
||||
@@ -903,6 +938,7 @@ export const secretScanningV2ServiceFactory = ({
|
||||
findSecretScanningConfigByProjectId,
|
||||
upsertSecretScanningConfig,
|
||||
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue),
|
||||
bitbucket: bitbucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService)
|
||||
bitbucket: bitbucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService),
|
||||
gitlab: gitlabSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService)
|
||||
};
|
||||
};
|
||||
|
@@ -21,14 +21,25 @@ import {
|
||||
TGitHubFinding,
|
||||
TQueueGitHubResourceDiffScan
|
||||
} from "@app/ee/services/secret-scanning-v2/github";
|
||||
import {
|
||||
TGitLabDataSource,
|
||||
TGitLabDataSourceCredentials,
|
||||
TGitLabDataSourceInput,
|
||||
TGitLabDataSourceListItem,
|
||||
TGitLabDataSourceWithConnection,
|
||||
TGitLabFinding,
|
||||
TQueueGitLabResourceDiffScan
|
||||
} from "@app/ee/services/secret-scanning-v2/gitlab";
|
||||
import { TSecretScanningV2DALFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-dal";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
SecretScanningFindingStatus,
|
||||
SecretScanningScanStatus
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
export type TSecretScanningDataSource = TGitHubDataSource | TBitbucketDataSource;
|
||||
export type TSecretScanningDataSource = TGitHubDataSource | TBitbucketDataSource | TGitLabDataSource;
|
||||
|
||||
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
|
||||
lastScannedAt?: Date | null;
|
||||
@@ -52,15 +63,25 @@ export type TSecretScanningScanWithDetails = TSecretScanningScans & {
|
||||
|
||||
export type TSecretScanningDataSourceWithConnection =
|
||||
| TGitHubDataSourceWithConnection
|
||||
| TBitbucketDataSourceWithConnection;
|
||||
| TBitbucketDataSourceWithConnection
|
||||
| TGitLabDataSourceWithConnection;
|
||||
|
||||
export type TSecretScanningDataSourceInput = TGitHubDataSourceInput | TBitbucketDataSourceInput;
|
||||
export type TSecretScanningDataSourceInput =
|
||||
| TGitHubDataSourceInput
|
||||
| TBitbucketDataSourceInput
|
||||
| TGitLabDataSourceInput;
|
||||
|
||||
export type TSecretScanningDataSourceListItem = TGitHubDataSourceListItem | TBitbucketDataSourceListItem;
|
||||
export type TSecretScanningDataSourceListItem =
|
||||
| TGitHubDataSourceListItem
|
||||
| TBitbucketDataSourceListItem
|
||||
| TGitLabDataSourceListItem;
|
||||
|
||||
export type TSecretScanningDataSourceCredentials = TBitbucketDataSourceCredentials | undefined;
|
||||
export type TSecretScanningDataSourceCredentials =
|
||||
| TBitbucketDataSourceCredentials
|
||||
| TGitLabDataSourceCredentials
|
||||
| undefined;
|
||||
|
||||
export type TSecretScanningFinding = TGitHubFinding | TBitbucketFinding;
|
||||
export type TSecretScanningFinding = TGitHubFinding | TBitbucketFinding | TGitLabFinding;
|
||||
|
||||
export type TListSecretScanningDataSourcesByProjectId = {
|
||||
projectId: string;
|
||||
@@ -112,7 +133,10 @@ export type TQueueSecretScanningDataSourceFullScan = {
|
||||
scanId: string;
|
||||
};
|
||||
|
||||
export type TQueueSecretScanningResourceDiffScan = TQueueGitHubResourceDiffScan | TQueueBitbucketResourceDiffScan;
|
||||
export type TQueueSecretScanningResourceDiffScan =
|
||||
| TQueueGitHubResourceDiffScan
|
||||
| TQueueBitbucketResourceDiffScan
|
||||
| TQueueGitLabResourceDiffScan;
|
||||
|
||||
export type TQueueSecretScanningSendNotification = {
|
||||
dataSource: TSecretScanningDataSources;
|
||||
@@ -170,6 +194,11 @@ export type TSecretScanningFactoryInitialize<
|
||||
callback: (parameters: { credentials?: C; externalId?: string }) => Promise<TSecretScanningDataSourceRaw>
|
||||
) => Promise<TSecretScanningDataSourceRaw>;
|
||||
|
||||
export type TSecretScanningFactoryValidateConfigUpdate<
|
||||
C extends TSecretScanningDataSourceInput["config"],
|
||||
T extends TSecretScanningDataSourceWithConnection
|
||||
> = (params: { config: C; dataSource: T }) => Promise<void>;
|
||||
|
||||
export type TSecretScanningFactoryPostInitialization<
|
||||
P extends TSecretScanningDataSourceInput,
|
||||
T extends TSecretScanningDataSourceWithConnection["connection"] | undefined = undefined,
|
||||
@@ -181,17 +210,23 @@ export type TSecretScanningFactoryTeardown<
|
||||
C extends TSecretScanningDataSourceCredentials = undefined
|
||||
> = (params: { dataSource: T; credentials: C }) => Promise<void>;
|
||||
|
||||
export type TSecretScanningFactoryParams = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export type TSecretScanningFactory<
|
||||
T extends TSecretScanningDataSourceWithConnection,
|
||||
P extends TQueueSecretScanningResourceDiffScan["payload"],
|
||||
I extends TSecretScanningDataSourceInput,
|
||||
C extends TSecretScanningDataSourceCredentials | undefined = undefined
|
||||
> = () => {
|
||||
> = (params: TSecretScanningFactoryParams) => {
|
||||
listRawResources: TSecretScanningFactoryListRawResources<T>;
|
||||
getFullScanPath: TSecretScanningFactoryGetFullScanPath<T>;
|
||||
initialize: TSecretScanningFactoryInitialize<I, T["connection"] | undefined, C>;
|
||||
postInitialization: TSecretScanningFactoryPostInitialization<I, T["connection"] | undefined, C>;
|
||||
teardown: TSecretScanningFactoryTeardown<T, C>;
|
||||
validateConfigUpdate: TSecretScanningFactoryValidateConfigUpdate<I["config"], T>;
|
||||
getDiffScanResourcePayload: TSecretScanningFactoryGetDiffScanResourcePayload<P>;
|
||||
getDiffScanFindingsPayload: TSecretScanningFactoryGetDiffScanFindingsPayload<T, P>;
|
||||
};
|
||||
|
@@ -2,10 +2,12 @@ import { z } from "zod";
|
||||
|
||||
import { BitbucketDataSourceSchema, BitbucketFindingSchema } from "@app/ee/services/secret-scanning-v2/bitbucket";
|
||||
import { GitHubDataSourceSchema, GitHubFindingSchema } from "@app/ee/services/secret-scanning-v2/github";
|
||||
import { GitLabDataSourceSchema, GitLabFindingSchema } from "@app/ee/services/secret-scanning-v2/gitlab";
|
||||
|
||||
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
|
||||
GitHubDataSourceSchema,
|
||||
BitbucketDataSourceSchema
|
||||
BitbucketDataSourceSchema,
|
||||
GitLabDataSourceSchema
|
||||
]);
|
||||
|
||||
export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType", [
|
||||
@@ -18,5 +20,10 @@ export const SecretScanningFindingSchema = z.discriminatedUnion("dataSourceType"
|
||||
JSON.stringify({
|
||||
title: "Bitbucket"
|
||||
})
|
||||
),
|
||||
GitLabFindingSchema.describe(
|
||||
JSON.stringify({
|
||||
title: "GitLab"
|
||||
})
|
||||
)
|
||||
]);
|
||||
|
@@ -664,6 +664,10 @@ export const ORGANIZATIONS = {
|
||||
organizationId: "The ID of the organization to delete the membership from.",
|
||||
membershipId: "The ID of the membership to delete."
|
||||
},
|
||||
BULK_DELETE_USER_MEMBERSHIPS: {
|
||||
organizationId: "The ID of the organization to delete the memberships from.",
|
||||
membershipIds: "The IDs of the memberships to delete."
|
||||
},
|
||||
LIST_IDENTITY_MEMBERSHIPS: {
|
||||
orgId: "The ID of the organization to get identity memberships from.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
|
||||
@@ -2253,7 +2257,9 @@ export const AppConnections = {
|
||||
AZURE_DEVOPS: {
|
||||
code: "The OAuth code to use to connect with Azure DevOps.",
|
||||
tenantId: "The Tenant ID to use to connect with Azure DevOps.",
|
||||
orgName: "The Organization name to use to connect with Azure DevOps."
|
||||
orgName: "The Organization name to use to connect with Azure DevOps.",
|
||||
clientId: "The Client ID to use to connect with Azure Client Secrets.",
|
||||
clientSecret: "The Client Secret to use to connect with Azure Client Secrets."
|
||||
},
|
||||
OCI: {
|
||||
userOcid: "The OCID (Oracle Cloud Identifier) of the user making the request.",
|
||||
@@ -2400,12 +2406,18 @@ export const SecretSyncs = {
|
||||
env: "The name of the GitHub environment."
|
||||
},
|
||||
AZURE_KEY_VAULT: {
|
||||
vaultBaseUrl: "The base URL of the Azure Key Vault to sync secrets to. Example: https://example.vault.azure.net/"
|
||||
vaultBaseUrl: "The base URL of the Azure Key Vault to sync secrets to. Example: https://example.vault.azure.net/",
|
||||
tenantId: "The Tenant ID to use to connect with Azure Client Secrets.",
|
||||
clientId: "The Client ID to use to connect with Azure Client Secrets.",
|
||||
clientSecret: "The Client Secret to use to connect with Azure Client Secrets."
|
||||
},
|
||||
AZURE_APP_CONFIGURATION: {
|
||||
configurationUrl:
|
||||
"The URL of the Azure App Configuration to sync secrets to. Example: https://example.azconfig.io/",
|
||||
label: "An optional label to assign to secrets created in Azure App Configuration."
|
||||
label: "An optional label to assign to secrets created in Azure App Configuration.",
|
||||
tenantId: "The Tenant ID to use to connect with Azure Client Secrets.",
|
||||
clientId: "The Client ID to use to connect with Azure Client Secrets.",
|
||||
clientSecret: "The Client Secret to use to connect with Azure Client Secrets."
|
||||
},
|
||||
AZURE_DEVOPS: {
|
||||
devopsProjectId: "The ID of the Azure DevOps project to sync secrets to.",
|
||||
@@ -2702,6 +2714,14 @@ export const SecretScanningDataSources = {
|
||||
GITHUB: {
|
||||
includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).'
|
||||
},
|
||||
GITLAB: {
|
||||
includeProjects: 'The projects to include when scanning. Defaults to all projects (["*"]).',
|
||||
scope: "The GitLab scope scanning should occur at (project or group level).",
|
||||
projectId: "The ID of the project to scan.",
|
||||
projectName: "The name of the project to scan.",
|
||||
groupId: "The ID of the group to scan projects from.",
|
||||
groupName: "The name of the group to scan projects from."
|
||||
},
|
||||
BITBUCKET: {
|
||||
workspaceSlug: "The workspace to scan.",
|
||||
includeRepos: 'The repositories to include when scanning. Defaults to all repositories (["*"]).'
|
||||
|
@@ -496,7 +496,7 @@ export const overwriteSchema: {
|
||||
]
|
||||
},
|
||||
azureAppConfiguration: {
|
||||
name: "Azure App Configuration",
|
||||
name: "Azure App Connection: App Configuration",
|
||||
fields: [
|
||||
{
|
||||
key: "INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID",
|
||||
@@ -509,7 +509,7 @@ export const overwriteSchema: {
|
||||
]
|
||||
},
|
||||
azureKeyVault: {
|
||||
name: "Azure Key Vault",
|
||||
name: "Azure App Connection: Key Vault",
|
||||
fields: [
|
||||
{
|
||||
key: "INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID",
|
||||
@@ -522,7 +522,7 @@ export const overwriteSchema: {
|
||||
]
|
||||
},
|
||||
azureClientSecrets: {
|
||||
name: "Azure Client Secrets",
|
||||
name: "Azure App Connection: Client Secrets",
|
||||
fields: [
|
||||
{
|
||||
key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID",
|
||||
@@ -535,7 +535,7 @@ export const overwriteSchema: {
|
||||
]
|
||||
},
|
||||
azureDevOps: {
|
||||
name: "Azure DevOps",
|
||||
name: "Azure App Connection: DevOps",
|
||||
fields: [
|
||||
{
|
||||
key: "INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID",
|
||||
|
@@ -11,3 +11,5 @@ export const UserPrincipalNameRegex = new RE2(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9._-]
|
||||
export const LdapUrlRegex = new RE2(/^ldaps?:\/\//);
|
||||
|
||||
export const BasicRepositoryRegex = new RE2(/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/);
|
||||
|
||||
export const GitLabProjectRegex = new RE2(/^[a-zA-Z0-9._-]+(?:\/[a-zA-Z0-9._-]+)+$/);
|
||||
|
@@ -4,6 +4,8 @@ import { Probot } from "probot";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TBitbucketPushEvent } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-types";
|
||||
import { TGitLabDataSourcePushEventPayload } from "@app/ee/services/secret-scanning-v2/gitlab";
|
||||
import { GitLabWebHookEvent } from "@app/ee/services/secret-scanning-v2/gitlab/gitlab-secret-scanning-enums";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
@@ -113,4 +115,36 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide
|
||||
return res.send("ok");
|
||||
}
|
||||
});
|
||||
|
||||
// gitlab push event webhook
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/gitlab",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
const event = req.headers["x-gitlab-event"] as GitLabWebHookEvent;
|
||||
const token = req.headers["x-gitlab-token"] as string;
|
||||
const dataSourceId = req.headers["x-data-source-id"] as string;
|
||||
|
||||
if (event !== GitLabWebHookEvent.Push) {
|
||||
return res.status(400).send({ message: `Event type not supported: ${event as string}` });
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).send({ message: "Unauthorized: Missing token" });
|
||||
}
|
||||
|
||||
if (!dataSourceId) return res.status(400).send({ message: "Data Source ID header is required" });
|
||||
|
||||
await server.services.secretScanningV2.gitlab.handlePushEvent({
|
||||
dataSourceId,
|
||||
payload: req.body as TGitLabDataSourcePushEventPayload,
|
||||
token
|
||||
});
|
||||
|
||||
return res.send("ok");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -1933,7 +1933,8 @@ export const registerRoutes = async (
|
||||
projectMembershipDAL,
|
||||
smtpService,
|
||||
kmsService,
|
||||
keyStore
|
||||
keyStore,
|
||||
appConnectionDAL
|
||||
});
|
||||
|
||||
const secretScanningV2Service = secretScanningV2ServiceFactory({
|
||||
@@ -1942,7 +1943,8 @@ export const registerRoutes = async (
|
||||
licenseService,
|
||||
secretScanningV2DAL,
|
||||
secretScanningV2Queue,
|
||||
kmsService
|
||||
kmsService,
|
||||
appConnectionDAL
|
||||
});
|
||||
|
||||
// setup the communication with license key server
|
||||
|
@@ -464,6 +464,42 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/user-management/users",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
userIds: z.string().array()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
users: UsersSchema.pick({
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
id: true
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: (req, res, done) => {
|
||||
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
|
||||
verifySuperAdmin(req, res, done);
|
||||
});
|
||||
},
|
||||
handler: async (req) => {
|
||||
const users = await server.services.superAdmin.deleteUsers(req.body.userIds);
|
||||
|
||||
return {
|
||||
users
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/user-management/users/:userId/admin-access",
|
||||
|
@@ -264,6 +264,48 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:organizationId/memberships",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.Organizations],
|
||||
description: "Bulk delete organization user memberships",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
organizationId: z.string().trim().describe(ORGANIZATIONS.BULK_DELETE_USER_MEMBERSHIPS.organizationId)
|
||||
}),
|
||||
body: z.object({
|
||||
membershipIds: z.string().trim().array().describe(ORGANIZATIONS.BULK_DELETE_USER_MEMBERSHIPS.membershipIds)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
memberships: OrgMembershipsSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
if (req.auth.actor !== ActorType.USER) return;
|
||||
|
||||
const memberships = await server.services.org.bulkDeleteOrgMemberships({
|
||||
userId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId,
|
||||
membershipIds: req.body.membershipIds,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return { memberships };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
// TODO: re-think endpoint structure in future so users only need to pass in membershipId bc organizationId is redundant
|
||||
method: "GET",
|
||||
|
@@ -1,3 +1,4 @@
|
||||
export enum AzureAppConfigurationConnectionMethod {
|
||||
OAuth = "oauth"
|
||||
OAuth = "oauth",
|
||||
ClientSecret = "client-secret"
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
@@ -19,7 +20,10 @@ export const getAzureAppConfigurationConnectionListItem = () => {
|
||||
return {
|
||||
name: "Azure App Configuration" as const,
|
||||
app: AppConnection.AzureAppConfiguration as const,
|
||||
methods: Object.values(AzureAppConfigurationConnectionMethod) as [AzureAppConfigurationConnectionMethod.OAuth],
|
||||
methods: Object.values(AzureAppConfigurationConnectionMethod) as [
|
||||
AzureAppConfigurationConnectionMethod.OAuth,
|
||||
AzureAppConfigurationConnectionMethod.ClientSecret
|
||||
],
|
||||
oauthClientId: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID
|
||||
};
|
||||
};
|
||||
@@ -35,71 +39,111 @@ export const validateAzureAppConfigurationConnectionCredentials = async (
|
||||
SITE_URL
|
||||
} = getConfig();
|
||||
|
||||
if (
|
||||
!INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID ||
|
||||
!INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET
|
||||
) {
|
||||
throw new InternalServerError({
|
||||
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
||||
let tokenError: AxiosError | null = null;
|
||||
|
||||
try {
|
||||
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: inputCredentials.code,
|
||||
scope: `openid offline_access https://azconfig.io/.default`,
|
||||
client_id: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET,
|
||||
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
|
||||
})
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
tokenError = e;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenError) {
|
||||
if (tokenError instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to get access token: ${
|
||||
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||
}`
|
||||
});
|
||||
} else {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get access token"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!tokenResp) {
|
||||
throw new InternalServerError({
|
||||
message: `Failed to get access token: Token was empty with no error`
|
||||
});
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||
if (
|
||||
!INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID ||
|
||||
!INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET
|
||||
) {
|
||||
throw new InternalServerError({
|
||||
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
||||
let tokenError: AxiosError | null = null;
|
||||
const oauthCredentials = inputCredentials as { code: string; tenantId?: string };
|
||||
try {
|
||||
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", oauthCredentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: oauthCredentials.code,
|
||||
scope: `openid offline_access https://azconfig.io/.default`,
|
||||
client_id: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET,
|
||||
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
|
||||
})
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
tokenError = e;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenError) {
|
||||
if (tokenError instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to get access token: ${
|
||||
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||
}`
|
||||
});
|
||||
} else {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get access token"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!tokenResp) {
|
||||
throw new InternalServerError({
|
||||
message: `Failed to get access token: Token was empty with no error`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tenantId: inputCredentials.tenantId,
|
||||
tenantId: oauthCredentials.tenantId,
|
||||
accessToken: tokenResp.data.access_token,
|
||||
refreshToken: tokenResp.data.refresh_token,
|
||||
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
||||
};
|
||||
|
||||
case AzureAppConfigurationConnectionMethod.ClientSecret:
|
||||
const { tenantId, clientId, clientSecret } = inputCredentials as {
|
||||
tenantId: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
try {
|
||||
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
scope: `https://azconfig.io/.default`,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
tenantId,
|
||||
accessToken: clientData.access_token,
|
||||
expiresAt: Date.now() + clientData.expires_in * 1000,
|
||||
clientId,
|
||||
clientSecret
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to get access token: ${
|
||||
(e?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||
}`
|
||||
});
|
||||
} else {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get access token"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new InternalServerError({
|
||||
message: `Unhandled Azure connection method: ${method as AzureAppConfigurationConnectionMethod}`
|
||||
message: `Unhandled Azure App Configuration connection method: ${method as AzureAppConfigurationConnectionMethod}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -22,6 +22,29 @@ export const AzureAppConfigurationConnectionOAuthOutputCredentialsSchema = z.obj
|
||||
expiresAt: z.number()
|
||||
});
|
||||
|
||||
export const AzureAppConfigurationConnectionClientSecretInputCredentialsSchema = z.object({
|
||||
clientId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.trim()
|
||||
.min(1, "Client ID required")
|
||||
.max(50, "Client ID must be at most 50 characters long"),
|
||||
clientSecret: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client Secret required")
|
||||
.max(50, "Client Secret must be at most 50 characters long"),
|
||||
tenantId: z.string().uuid().trim().min(1, "Tenant ID required")
|
||||
});
|
||||
|
||||
export const AzureAppConfigurationConnectionClientSecretOutputCredentialsSchema = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
tenantId: z.string(),
|
||||
accessToken: z.string(),
|
||||
expiresAt: z.number()
|
||||
});
|
||||
|
||||
export const ValidateAzureAppConfigurationConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
@@ -30,6 +53,14 @@ export const ValidateAzureAppConfigurationConnectionCredentialsSchema = z.discri
|
||||
credentials: AzureAppConfigurationConnectionOAuthInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureAppConfiguration).credentials
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
method: z
|
||||
.literal(AzureAppConfigurationConnectionMethod.ClientSecret)
|
||||
.describe(AppConnections.CREATE(AppConnection.AzureAppConfiguration).method),
|
||||
credentials: AzureAppConfigurationConnectionClientSecretInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureAppConfiguration).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -39,9 +70,13 @@ export const CreateAzureAppConfigurationConnectionSchema = ValidateAzureAppConfi
|
||||
|
||||
export const UpdateAzureAppConfigurationConnectionSchema = z
|
||||
.object({
|
||||
credentials: AzureAppConfigurationConnectionOAuthInputCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.AzureAppConfiguration).credentials
|
||||
)
|
||||
credentials: z
|
||||
.union([
|
||||
AzureAppConfigurationConnectionOAuthInputCredentialsSchema,
|
||||
AzureAppConfigurationConnectionClientSecretInputCredentialsSchema
|
||||
])
|
||||
.optional()
|
||||
.describe(AppConnections.UPDATE(AppConnection.AzureAppConfiguration).credentials)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureAppConfiguration));
|
||||
|
||||
@@ -55,6 +90,10 @@ export const AzureAppConfigurationConnectionSchema = z.intersection(
|
||||
z.object({
|
||||
method: z.literal(AzureAppConfigurationConnectionMethod.OAuth),
|
||||
credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AzureAppConfigurationConnectionMethod.ClientSecret),
|
||||
credentials: AzureAppConfigurationConnectionClientSecretOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -65,6 +104,13 @@ export const SanitizedAzureAppConfigurationConnectionSchema = z.discriminatedUni
|
||||
credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema.pick({
|
||||
tenantId: true
|
||||
})
|
||||
}),
|
||||
BaseAzureAppConfigurationConnectionSchema.extend({
|
||||
method: z.literal(AzureAppConfigurationConnectionMethod.ClientSecret),
|
||||
credentials: AzureAppConfigurationConnectionClientSecretOutputCredentialsSchema.pick({
|
||||
clientId: true,
|
||||
tenantId: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
AzureAppConfigurationConnectionClientSecretOutputCredentialsSchema,
|
||||
AzureAppConfigurationConnectionOAuthOutputCredentialsSchema,
|
||||
AzureAppConfigurationConnectionSchema,
|
||||
CreateAzureAppConfigurationConnectionSchema,
|
||||
@@ -39,3 +40,7 @@ export type ExchangeCodeAzureResponse = {
|
||||
export type TAzureAppConfigurationConnectionCredentials = z.infer<
|
||||
typeof AzureAppConfigurationConnectionOAuthOutputCredentialsSchema
|
||||
>;
|
||||
|
||||
export type TAzureAppConfigurationConnectionClientSecretCredentials = z.infer<
|
||||
typeof AzureAppConfigurationConnectionClientSecretOutputCredentialsSchema
|
||||
>;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export enum AzureDevOpsConnectionMethod {
|
||||
OAuth = "oauth",
|
||||
AccessToken = "access-token"
|
||||
AccessToken = "access-token",
|
||||
ClientSecret = "client-secret"
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ import { AppConnection } from "../app-connection-enums";
|
||||
import { AzureDevOpsConnectionMethod } from "./azure-devops-enums";
|
||||
import {
|
||||
ExchangeCodeAzureResponse,
|
||||
TAzureDevOpsConnectionClientSecretCredentials,
|
||||
TAzureDevOpsConnectionConfig,
|
||||
TAzureDevOpsConnectionCredentials
|
||||
} from "./azure-devops-types";
|
||||
@@ -30,7 +31,8 @@ export const getAzureDevopsConnectionListItem = () => {
|
||||
app: AppConnection.AzureDevOps as const,
|
||||
methods: Object.values(AzureDevOpsConnectionMethod) as [
|
||||
AzureDevOpsConnectionMethod.OAuth,
|
||||
AzureDevOpsConnectionMethod.AccessToken
|
||||
AzureDevOpsConnectionMethod.AccessToken,
|
||||
AzureDevOpsConnectionMethod.ClientSecret
|
||||
],
|
||||
oauthClientId: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID
|
||||
};
|
||||
@@ -53,11 +55,7 @@ export const getAzureDevopsConnection = async (
|
||||
});
|
||||
}
|
||||
|
||||
const credentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureDevOpsConnectionCredentials;
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Handle different connection methods
|
||||
switch (appConnection.method) {
|
||||
@@ -69,12 +67,17 @@ export const getAzureDevopsConnection = async (
|
||||
});
|
||||
}
|
||||
|
||||
if (!("refreshToken" in credentials)) {
|
||||
const oauthCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureDevOpsConnectionCredentials;
|
||||
|
||||
if (!("refreshToken" in oauthCredentials)) {
|
||||
throw new BadRequestError({ message: "Invalid OAuth credentials" });
|
||||
}
|
||||
|
||||
const { refreshToken, tenantId } = credentials;
|
||||
const currentTime = Date.now();
|
||||
const { refreshToken, tenantId } = oauthCredentials;
|
||||
|
||||
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||
@@ -87,29 +90,75 @@ export const getAzureDevopsConnection = async (
|
||||
})
|
||||
);
|
||||
|
||||
const updatedCredentials = {
|
||||
...credentials,
|
||||
const updatedOAuthCredentials = {
|
||||
...oauthCredentials,
|
||||
accessToken: data.access_token,
|
||||
expiresAt: currentTime + data.expires_in * 1000,
|
||||
refreshToken: data.refresh_token
|
||||
};
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
const encryptedOAuthCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedOAuthCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedOAuthCredentials });
|
||||
|
||||
return data.access_token;
|
||||
|
||||
case AzureDevOpsConnectionMethod.AccessToken:
|
||||
if (!("accessToken" in credentials)) {
|
||||
const accessTokenCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as { accessToken: string };
|
||||
|
||||
if (!("accessToken" in accessTokenCredentials)) {
|
||||
throw new BadRequestError({ message: "Invalid API token credentials" });
|
||||
}
|
||||
// For access token, return the basic auth token directly
|
||||
return credentials.accessToken;
|
||||
return accessTokenCredentials.accessToken;
|
||||
|
||||
case AzureDevOpsConnectionMethod.ClientSecret:
|
||||
const clientSecretCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureDevOpsConnectionClientSecretCredentials;
|
||||
|
||||
const { accessToken, expiresAt, clientId, clientSecret, tenantId: clientTenantId } = clientSecretCredentials;
|
||||
|
||||
// Check if token is still valid (with 5 minute buffer)
|
||||
if (accessToken && expiresAt && expiresAt > currentTime + 300000) {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", clientTenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
scope: `https://app.vssps.visualstudio.com/.default`,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
})
|
||||
);
|
||||
|
||||
const updatedClientCredentials = {
|
||||
...clientSecretCredentials,
|
||||
accessToken: clientData.access_token,
|
||||
expiresAt: currentTime + clientData.expires_in * 1000
|
||||
};
|
||||
|
||||
const encryptedClientCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedClientCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials });
|
||||
|
||||
return clientData.access_token;
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: `Unsupported connection method` });
|
||||
@@ -138,7 +187,7 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev
|
||||
let tokenError: AxiosError | null = null;
|
||||
|
||||
try {
|
||||
const oauthCredentials = inputCredentials as { code: string; tenantId: string };
|
||||
const oauthCredentials = inputCredentials as { code: string; tenantId: string; orgName: string };
|
||||
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", oauthCredentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
@@ -262,9 +311,67 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev
|
||||
});
|
||||
}
|
||||
|
||||
case AzureDevOpsConnectionMethod.ClientSecret:
|
||||
const { tenantId, clientId, clientSecret, orgName } = inputCredentials as {
|
||||
tenantId: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
orgName: string;
|
||||
};
|
||||
|
||||
try {
|
||||
// First, get the access token using client credentials flow
|
||||
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
scope: `https://app.vssps.visualstudio.com/.default`,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
})
|
||||
);
|
||||
|
||||
// Validate access to the specific organization
|
||||
const response = await request.get(
|
||||
`${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/_apis/projects?api-version=7.2-preview.2&$top=1`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${clientData.access_token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate connection to organization '${orgName}': ${response.status}`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tenantId,
|
||||
clientId,
|
||||
clientSecret,
|
||||
orgName,
|
||||
accessToken: clientData.access_token,
|
||||
expiresAt: Date.now() + clientData.expires_in * 1000
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to authenticate with Azure DevOps using client credentials: ${
|
||||
(e?.response?.data as { error_description?: string })?.error_description || e.message
|
||||
}`
|
||||
});
|
||||
} else {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to validate Azure DevOps client credentials"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new InternalServerError({
|
||||
message: `Unhandled Azure connection method: ${method as AzureDevOpsConnectionMethod}`
|
||||
message: `Unhandled Azure DevOps connection method: ${method as AzureDevOpsConnectionMethod}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -38,6 +38,42 @@ export const AzureDevOpsConnectionAccessTokenOutputCredentialsSchema = z.object(
|
||||
orgName: z.string()
|
||||
});
|
||||
|
||||
export const AzureDevOpsConnectionClientSecretInputCredentialsSchema = z.object({
|
||||
clientId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.trim()
|
||||
.min(1, "Client ID required")
|
||||
.max(50, "Client ID must be at most 50 characters long")
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_DEVOPS.clientId),
|
||||
clientSecret: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client Secret required")
|
||||
.max(50, "Client Secret must be at most 50 characters long")
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_DEVOPS.clientSecret),
|
||||
tenantId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.trim()
|
||||
.min(1, "Tenant ID required")
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_DEVOPS.tenantId),
|
||||
orgName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Organization name required")
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_DEVOPS.orgName)
|
||||
});
|
||||
|
||||
export const AzureDevOpsConnectionClientSecretOutputCredentialsSchema = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
tenantId: z.string(),
|
||||
orgName: z.string(),
|
||||
accessToken: z.string(),
|
||||
expiresAt: z.number()
|
||||
});
|
||||
|
||||
export const ValidateAzureDevOpsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
@@ -54,6 +90,14 @@ export const ValidateAzureDevOpsConnectionCredentialsSchema = z.discriminatedUni
|
||||
credentials: AzureDevOpsConnectionAccessTokenInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureDevOps).credentials
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
method: z
|
||||
.literal(AzureDevOpsConnectionMethod.ClientSecret)
|
||||
.describe(AppConnections.CREATE(AppConnection.AzureDevOps).method),
|
||||
credentials: AzureDevOpsConnectionClientSecretInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureDevOps).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -64,7 +108,11 @@ export const CreateAzureDevOpsConnectionSchema = ValidateAzureDevOpsConnectionCr
|
||||
export const UpdateAzureDevOpsConnectionSchema = z
|
||||
.object({
|
||||
credentials: z
|
||||
.union([AzureDevOpsConnectionOAuthInputCredentialsSchema, AzureDevOpsConnectionAccessTokenInputCredentialsSchema])
|
||||
.union([
|
||||
AzureDevOpsConnectionOAuthInputCredentialsSchema,
|
||||
AzureDevOpsConnectionAccessTokenInputCredentialsSchema,
|
||||
AzureDevOpsConnectionClientSecretInputCredentialsSchema
|
||||
])
|
||||
.optional()
|
||||
.describe(AppConnections.UPDATE(AppConnection.AzureDevOps).credentials)
|
||||
})
|
||||
@@ -84,6 +132,10 @@ export const AzureDevOpsConnectionSchema = z.intersection(
|
||||
z.object({
|
||||
method: z.literal(AzureDevOpsConnectionMethod.AccessToken),
|
||||
credentials: AzureDevOpsConnectionAccessTokenOutputCredentialsSchema
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AzureDevOpsConnectionMethod.ClientSecret),
|
||||
credentials: AzureDevOpsConnectionClientSecretOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -101,6 +153,14 @@ export const SanitizedAzureDevOpsConnectionSchema = z.discriminatedUnion("method
|
||||
credentials: AzureDevOpsConnectionAccessTokenOutputCredentialsSchema.pick({
|
||||
orgName: true
|
||||
})
|
||||
}),
|
||||
BaseAzureDevOpsConnectionSchema.extend({
|
||||
method: z.literal(AzureDevOpsConnectionMethod.ClientSecret),
|
||||
credentials: AzureDevOpsConnectionClientSecretOutputCredentialsSchema.pick({
|
||||
clientId: true,
|
||||
tenantId: true,
|
||||
orgName: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
|
@@ -52,6 +52,11 @@ const getAuthHeaders = (appConnection: TAzureDevOpsConnection, accessToken: stri
|
||||
Authorization: `Basic ${basicAuthToken}`,
|
||||
Accept: "application/json"
|
||||
};
|
||||
case AzureDevOpsConnectionMethod.ClientSecret:
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
};
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unsupported connection method" });
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
AzureDevOpsConnectionClientSecretOutputCredentialsSchema,
|
||||
AzureDevOpsConnectionOAuthOutputCredentialsSchema,
|
||||
AzureDevOpsConnectionSchema,
|
||||
CreateAzureDevOpsConnectionSchema,
|
||||
@@ -27,6 +28,10 @@ export type TAzureDevOpsConnectionConfig = DiscriminativePick<
|
||||
|
||||
export type TAzureDevOpsConnectionCredentials = z.infer<typeof AzureDevOpsConnectionOAuthOutputCredentialsSchema>;
|
||||
|
||||
export type TAzureDevOpsConnectionClientSecretCredentials = z.infer<
|
||||
typeof AzureDevOpsConnectionClientSecretOutputCredentialsSchema
|
||||
>;
|
||||
|
||||
export interface ExchangeCodeAzureResponse {
|
||||
token_type: string;
|
||||
scope: string;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
export enum AzureKeyVaultConnectionMethod {
|
||||
OAuth = "oauth"
|
||||
OAuth = "oauth",
|
||||
ClientSecret = "client-secret"
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
@@ -16,25 +17,16 @@ import { AppConnection } from "../app-connection-enums";
|
||||
import { AzureKeyVaultConnectionMethod } from "./azure-key-vault-connection-enums";
|
||||
import {
|
||||
ExchangeCodeAzureResponse,
|
||||
TAzureKeyVaultConnectionClientSecretCredentials,
|
||||
TAzureKeyVaultConnectionConfig,
|
||||
TAzureKeyVaultConnectionCredentials
|
||||
} from "./azure-key-vault-connection-types";
|
||||
|
||||
export const getAzureConnectionAccessToken = async (
|
||||
connectionId: string,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const appCfg = getConfig();
|
||||
if (
|
||||
!appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID ||
|
||||
!appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: `Azure environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||
|
||||
if (!appConnection) {
|
||||
@@ -49,49 +41,101 @@ export const getAzureConnectionAccessToken = async (
|
||||
throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not a valid Azure connection` });
|
||||
}
|
||||
|
||||
const credentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureKeyVaultConnectionCredentials;
|
||||
const currentTime = Date.now();
|
||||
|
||||
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
scope: `openid offline_access`,
|
||||
client_id: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID,
|
||||
client_secret: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET,
|
||||
refresh_token: credentials.refreshToken
|
||||
})
|
||||
);
|
||||
switch (appConnection.method) {
|
||||
case AzureKeyVaultConnectionMethod.OAuth:
|
||||
const appCfg = getConfig();
|
||||
if (
|
||||
!appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID ||
|
||||
!appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: `Azure environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
const accessExpiresAt = new Date();
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||
const oauthCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureKeyVaultConnectionCredentials;
|
||||
|
||||
const updatedCredentials = {
|
||||
...credentials,
|
||||
accessToken: data.access_token,
|
||||
expiresAt: accessExpiresAt.getTime(),
|
||||
refreshToken: data.refresh_token
|
||||
};
|
||||
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", oauthCredentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
scope: `openid offline_access https://vault.azure.net/.default`,
|
||||
client_id: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID,
|
||||
client_secret: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET,
|
||||
refresh_token: oauthCredentials.refreshToken
|
||||
})
|
||||
);
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
const updatedOAuthCredentials = {
|
||||
...oauthCredentials,
|
||||
accessToken: data.access_token,
|
||||
expiresAt: currentTime + data.expires_in * 1000,
|
||||
refreshToken: data.refresh_token
|
||||
};
|
||||
|
||||
await appConnectionDAL.update(
|
||||
{ id: connectionId },
|
||||
{
|
||||
encryptedCredentials
|
||||
}
|
||||
);
|
||||
const encryptedOAuthCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedOAuthCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: data.access_token
|
||||
};
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedOAuthCredentials });
|
||||
|
||||
return {
|
||||
accessToken: data.access_token
|
||||
};
|
||||
|
||||
case AzureKeyVaultConnectionMethod.ClientSecret:
|
||||
const clientSecretCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureKeyVaultConnectionClientSecretCredentials;
|
||||
|
||||
const { accessToken, expiresAt, clientId, clientSecret, tenantId } = clientSecretCredentials;
|
||||
|
||||
// Check if token is still valid (with 5 minute buffer)
|
||||
if (accessToken && expiresAt && expiresAt > currentTime + 300000) {
|
||||
return { accessToken };
|
||||
}
|
||||
|
||||
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
scope: `https://vault.azure.net/.default`,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
})
|
||||
);
|
||||
|
||||
const updatedClientCredentials = {
|
||||
...clientSecretCredentials,
|
||||
accessToken: clientData.access_token,
|
||||
expiresAt: currentTime + clientData.expires_in * 1000
|
||||
};
|
||||
|
||||
const encryptedClientCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedClientCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials });
|
||||
|
||||
return { accessToken: clientData.access_token };
|
||||
|
||||
default:
|
||||
throw new InternalServerError({
|
||||
message: `Unhandled Azure Key Vault connection method: ${appConnection.method as AzureKeyVaultConnectionMethod}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getAzureKeyVaultConnectionListItem = () => {
|
||||
@@ -100,7 +144,10 @@ export const getAzureKeyVaultConnectionListItem = () => {
|
||||
return {
|
||||
name: "Azure Key Vault" as const,
|
||||
app: AppConnection.AzureKeyVault as const,
|
||||
methods: Object.values(AzureKeyVaultConnectionMethod) as [AzureKeyVaultConnectionMethod.OAuth],
|
||||
methods: Object.values(AzureKeyVaultConnectionMethod) as [
|
||||
AzureKeyVaultConnectionMethod.OAuth,
|
||||
AzureKeyVaultConnectionMethod.ClientSecret
|
||||
],
|
||||
oauthClientId: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID
|
||||
};
|
||||
};
|
||||
@@ -111,68 +158,108 @@ export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureK
|
||||
const { INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID, INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET, SITE_URL } =
|
||||
getConfig();
|
||||
|
||||
if (!INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || !INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET) {
|
||||
throw new InternalServerError({
|
||||
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
||||
let tokenError: AxiosError | null = null;
|
||||
|
||||
try {
|
||||
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: inputCredentials.code,
|
||||
scope: `openid offline_access https://vault.azure.net/.default`,
|
||||
client_id: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET,
|
||||
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
|
||||
})
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
tokenError = e;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenError) {
|
||||
if (tokenError instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to get access token: ${
|
||||
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||
}`
|
||||
});
|
||||
} else {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get access token"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!tokenResp) {
|
||||
throw new InternalServerError({
|
||||
message: `Failed to get access token: Token was empty with no error`
|
||||
});
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case AzureKeyVaultConnectionMethod.OAuth:
|
||||
if (!INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || !INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET) {
|
||||
throw new InternalServerError({
|
||||
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
||||
let tokenError: AxiosError | null = null;
|
||||
const oauthCredentials = inputCredentials as { code: string; tenantId?: string };
|
||||
try {
|
||||
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", oauthCredentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: oauthCredentials.code,
|
||||
scope: `openid offline_access https://vault.azure.net/.default`,
|
||||
client_id: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET,
|
||||
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
|
||||
})
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
tokenError = e;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenError) {
|
||||
if (tokenError instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to get access token: ${
|
||||
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||
}`
|
||||
});
|
||||
} else {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get access token"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!tokenResp) {
|
||||
throw new InternalServerError({
|
||||
message: `Failed to get access token: Token was empty with no error`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tenantId: inputCredentials.tenantId,
|
||||
tenantId: oauthCredentials.tenantId,
|
||||
accessToken: tokenResp.data.access_token,
|
||||
refreshToken: tokenResp.data.refresh_token,
|
||||
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
||||
};
|
||||
|
||||
case AzureKeyVaultConnectionMethod.ClientSecret:
|
||||
const { tenantId, clientId, clientSecret } = inputCredentials as {
|
||||
tenantId: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
try {
|
||||
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
scope: `https://vault.azure.net/.default`,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
tenantId,
|
||||
accessToken: clientData.access_token,
|
||||
expiresAt: Date.now() + clientData.expires_in * 1000,
|
||||
clientId,
|
||||
clientSecret
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to get access token: ${
|
||||
(e?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||
}`
|
||||
});
|
||||
} else {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get access token"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new InternalServerError({
|
||||
message: `Unhandled Azure connection method: ${method as AzureKeyVaultConnectionMethod}`
|
||||
message: `Unhandled Azure Key Vault connection method: ${method as AzureKeyVaultConnectionMethod}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -22,6 +22,29 @@ export const AzureKeyVaultConnectionOAuthOutputCredentialsSchema = z.object({
|
||||
expiresAt: z.number()
|
||||
});
|
||||
|
||||
export const AzureKeyVaultConnectionClientSecretInputCredentialsSchema = z.object({
|
||||
clientId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.trim()
|
||||
.min(1, "Client ID required")
|
||||
.max(50, "Client ID must be at most 50 characters long"),
|
||||
clientSecret: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client Secret required")
|
||||
.max(50, "Client Secret must be at most 50 characters long"),
|
||||
tenantId: z.string().uuid().trim().min(1, "Tenant ID required")
|
||||
});
|
||||
|
||||
export const AzureKeyVaultConnectionClientSecretOutputCredentialsSchema = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
tenantId: z.string(),
|
||||
accessToken: z.string(),
|
||||
expiresAt: z.number()
|
||||
});
|
||||
|
||||
export const ValidateAzureKeyVaultConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
@@ -30,6 +53,14 @@ export const ValidateAzureKeyVaultConnectionCredentialsSchema = z.discriminatedU
|
||||
credentials: AzureKeyVaultConnectionOAuthInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureKeyVault).credentials
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
method: z
|
||||
.literal(AzureKeyVaultConnectionMethod.ClientSecret)
|
||||
.describe(AppConnections.CREATE(AppConnection.AzureKeyVault).method),
|
||||
credentials: AzureKeyVaultConnectionClientSecretInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureKeyVault).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -39,9 +70,13 @@ export const CreateAzureKeyVaultConnectionSchema = ValidateAzureKeyVaultConnecti
|
||||
|
||||
export const UpdateAzureKeyVaultConnectionSchema = z
|
||||
.object({
|
||||
credentials: AzureKeyVaultConnectionOAuthInputCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.AzureKeyVault).credentials
|
||||
)
|
||||
credentials: z
|
||||
.union([
|
||||
AzureKeyVaultConnectionOAuthInputCredentialsSchema,
|
||||
AzureKeyVaultConnectionClientSecretInputCredentialsSchema
|
||||
])
|
||||
.optional()
|
||||
.describe(AppConnections.UPDATE(AppConnection.AzureKeyVault).credentials)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureKeyVault));
|
||||
|
||||
@@ -55,6 +90,10 @@ export const AzureKeyVaultConnectionSchema = z.intersection(
|
||||
z.object({
|
||||
method: z.literal(AzureKeyVaultConnectionMethod.OAuth),
|
||||
credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AzureKeyVaultConnectionMethod.ClientSecret),
|
||||
credentials: AzureKeyVaultConnectionClientSecretOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -65,6 +104,13 @@ export const SanitizedAzureKeyVaultConnectionSchema = z.discriminatedUnion("meth
|
||||
credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema.pick({
|
||||
tenantId: true
|
||||
})
|
||||
}),
|
||||
BaseAzureKeyVaultConnectionSchema.extend({
|
||||
method: z.literal(AzureKeyVaultConnectionMethod.ClientSecret),
|
||||
credentials: AzureKeyVaultConnectionClientSecretOutputCredentialsSchema.pick({
|
||||
clientId: true,
|
||||
tenantId: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
AzureKeyVaultConnectionClientSecretOutputCredentialsSchema,
|
||||
AzureKeyVaultConnectionOAuthOutputCredentialsSchema,
|
||||
AzureKeyVaultConnectionSchema,
|
||||
CreateAzureKeyVaultConnectionSchema,
|
||||
@@ -36,3 +37,7 @@ export type ExchangeCodeAzureResponse = {
|
||||
};
|
||||
|
||||
export type TAzureKeyVaultConnectionCredentials = z.infer<typeof AzureKeyVaultConnectionOAuthOutputCredentialsSchema>;
|
||||
|
||||
export type TAzureKeyVaultConnectionClientSecretCredentials = z.infer<
|
||||
typeof AzureKeyVaultConnectionClientSecretOutputCredentialsSchema
|
||||
>;
|
||||
|
@@ -222,6 +222,37 @@ export const validateGitLabConnectionCredentials = async (config: TGitLabConnect
|
||||
return inputCredentials;
|
||||
};
|
||||
|
||||
export const getGitLabConnectionClient = async (
|
||||
appConnection: TGitLabConnection,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
let { accessToken } = appConnection.credentials;
|
||||
|
||||
if (
|
||||
appConnection.method === GitLabConnectionMethod.OAuth &&
|
||||
appConnection.credentials.refreshToken &&
|
||||
new Date(appConnection.credentials.expiresAt) < new Date()
|
||||
) {
|
||||
accessToken = await refreshGitLabToken(
|
||||
appConnection.credentials.refreshToken,
|
||||
appConnection.id,
|
||||
appConnection.orgId,
|
||||
appConnectionDAL,
|
||||
kmsService,
|
||||
appConnection.credentials.instanceUrl
|
||||
);
|
||||
}
|
||||
|
||||
const client = await getGitLabClient(
|
||||
accessToken,
|
||||
appConnection.credentials.instanceUrl,
|
||||
appConnection.method === GitLabConnectionMethod.OAuth
|
||||
);
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
export const listGitLabProjects = async ({
|
||||
appConnection,
|
||||
appConnectionDAL,
|
||||
|
@@ -513,6 +513,21 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMembershipsById = async (ids: string[], orgId: string, tx?: Knex) => {
|
||||
try {
|
||||
const memberships = await (tx || db)(TableName.OrgMembership)
|
||||
.where({
|
||||
orgId
|
||||
})
|
||||
.whereIn("id", ids)
|
||||
.delete()
|
||||
.returning("*");
|
||||
return memberships;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Delete org memberships" });
|
||||
}
|
||||
};
|
||||
|
||||
const findMembership = async (
|
||||
filter: TFindFilter<TOrgMemberships>,
|
||||
{ offset, limit, sort, tx }: TFindOpt<TOrgMemberships> = {}
|
||||
@@ -634,6 +649,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
createMembership,
|
||||
updateMembershipById,
|
||||
deleteMembershipById,
|
||||
deleteMembershipsById,
|
||||
updateMembership
|
||||
});
|
||||
};
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
@@ -14,6 +15,19 @@ type TDeleteOrgMembership = {
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
type TDeleteOrgMemberships = {
|
||||
orgMembershipIds: string[];
|
||||
orgId: string;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipsById" | "transaction">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "delete" | "findProjectMembershipsByUserIds">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
export const deleteOrgMembershipFn = async ({
|
||||
@@ -24,11 +38,17 @@ export const deleteOrgMembershipFn = async ({
|
||||
projectUserAdditionalPrivilegeDAL,
|
||||
projectKeyDAL,
|
||||
userAliasDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
userId
|
||||
}: TDeleteOrgMembership) => {
|
||||
const deletedMembership = await orgDAL.transaction(async (tx) => {
|
||||
const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx);
|
||||
|
||||
if (userId && orgMembership.userId === userId) {
|
||||
// scott: this is temporary, we will add a leave org endpoint with proper handling to ensure org isn't abandoned/broken
|
||||
throw new BadRequestError({ message: "You cannot remove yourself from an organization" });
|
||||
}
|
||||
|
||||
if (!orgMembership.userId) {
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
return orgMembership;
|
||||
@@ -86,3 +106,88 @@ export const deleteOrgMembershipFn = async ({
|
||||
|
||||
return deletedMembership;
|
||||
};
|
||||
|
||||
export const deleteOrgMembershipsFn = async ({
|
||||
orgMembershipIds,
|
||||
orgId,
|
||||
orgDAL,
|
||||
projectMembershipDAL,
|
||||
projectUserAdditionalPrivilegeDAL,
|
||||
projectKeyDAL,
|
||||
userAliasDAL,
|
||||
licenseService,
|
||||
userId
|
||||
}: TDeleteOrgMemberships) => {
|
||||
const deletedMemberships = await orgDAL.transaction(async (tx) => {
|
||||
const orgMemberships = await orgDAL.deleteMembershipsById(orgMembershipIds, orgId, tx);
|
||||
|
||||
const membershipUserIds = orgMemberships
|
||||
.filter((member) => Boolean(member.userId))
|
||||
.map((member) => member.userId) as string[];
|
||||
|
||||
if (userId && membershipUserIds.includes(userId)) {
|
||||
// scott: this is temporary, we will add a leave org endpoint with proper handling to ensure org isn't abandoned/broken
|
||||
throw new BadRequestError({ message: "You cannot remove yourself from an organization" });
|
||||
}
|
||||
|
||||
if (!membershipUserIds.length) {
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
return orgMemberships;
|
||||
}
|
||||
|
||||
await userAliasDAL.delete(
|
||||
{
|
||||
$in: {
|
||||
userId: membershipUserIds
|
||||
},
|
||||
orgId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await projectUserAdditionalPrivilegeDAL.delete(
|
||||
{
|
||||
$in: {
|
||||
userId: membershipUserIds
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
// Get all the project memberships of the users in the organization
|
||||
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserIds(orgId, membershipUserIds);
|
||||
|
||||
// Delete all the project memberships of the users in the organization
|
||||
await projectMembershipDAL.delete(
|
||||
{
|
||||
$in: {
|
||||
id: projectMemberships.map((membership) => membership.id)
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
// Get all the project keys of the user in the organization
|
||||
const projectKeys = await projectKeyDAL.find({
|
||||
$in: {
|
||||
projectId: projectMemberships.map((membership) => membership.projectId),
|
||||
receiverId: membershipUserIds
|
||||
}
|
||||
});
|
||||
|
||||
// Delete all the project keys of the user in the organization
|
||||
await projectKeyDAL.delete(
|
||||
{
|
||||
$in: {
|
||||
id: projectKeys.map((key) => key.id)
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
return orgMemberships;
|
||||
});
|
||||
|
||||
return deletedMemberships;
|
||||
};
|
||||
|
@@ -75,10 +75,11 @@ import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
||||
import { TOrgBotDALFactory } from "./org-bot-dal";
|
||||
import { TOrgDALFactory } from "./org-dal";
|
||||
import { deleteOrgMembershipFn } from "./org-fns";
|
||||
import { deleteOrgMembershipFn, deleteOrgMembershipsFn } from "./org-fns";
|
||||
import { TOrgRoleDALFactory } from "./org-role-dal";
|
||||
import {
|
||||
TDeleteOrgMembershipDTO,
|
||||
TDeleteOrgMembershipsDTO,
|
||||
TFindAllWorkspacesDTO,
|
||||
TFindOrgMembersByEmailDTO,
|
||||
TGetOrgGroupsDTO,
|
||||
@@ -106,7 +107,13 @@ type TOrgServiceFactoryDep = {
|
||||
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
|
||||
projectMembershipDAL: Pick<
|
||||
TProjectMembershipDALFactory,
|
||||
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
|
||||
| "findProjectMembershipsByUserId"
|
||||
| "delete"
|
||||
| "create"
|
||||
| "find"
|
||||
| "insertMany"
|
||||
| "transaction"
|
||||
| "findProjectMembershipsByUserIds"
|
||||
>;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey" | "create">;
|
||||
orgMembershipDAL: Pick<
|
||||
@@ -1369,12 +1376,42 @@ export const orgServiceFactory = ({
|
||||
projectUserAdditionalPrivilegeDAL,
|
||||
projectKeyDAL,
|
||||
userAliasDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
userId
|
||||
});
|
||||
|
||||
return deletedMembership;
|
||||
};
|
||||
|
||||
const bulkDeleteOrgMemberships = async ({
|
||||
orgId,
|
||||
userId,
|
||||
membershipIds,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TDeleteOrgMembershipsDTO) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
|
||||
|
||||
if (membershipIds.includes(userId)) {
|
||||
throw new BadRequestError({ message: "You cannot delete your own organization membership" });
|
||||
}
|
||||
|
||||
const deletedMemberships = await deleteOrgMembershipsFn({
|
||||
orgMembershipIds: membershipIds,
|
||||
orgId,
|
||||
orgDAL,
|
||||
projectMembershipDAL,
|
||||
projectUserAdditionalPrivilegeDAL,
|
||||
projectKeyDAL,
|
||||
userAliasDAL,
|
||||
licenseService,
|
||||
userId
|
||||
});
|
||||
|
||||
return deletedMemberships;
|
||||
};
|
||||
|
||||
const listProjectMembershipsByOrgMembershipId = async ({
|
||||
orgMembershipId,
|
||||
orgId,
|
||||
@@ -1528,6 +1565,7 @@ export const orgServiceFactory = ({
|
||||
findOrgBySlug,
|
||||
resendOrgMemberInvitation,
|
||||
upgradePrivilegeSystem,
|
||||
notifyInvitedUsers
|
||||
notifyInvitedUsers,
|
||||
bulkDeleteOrgMemberships
|
||||
};
|
||||
};
|
||||
|
@@ -25,6 +25,14 @@ export type TDeleteOrgMembershipDTO = {
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
};
|
||||
|
||||
export type TDeleteOrgMembershipsDTO = {
|
||||
userId: string;
|
||||
orgId: string;
|
||||
membershipIds: string[];
|
||||
actorOrgId: string | undefined;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
};
|
||||
|
||||
export type TInviteUserToOrgDTO = {
|
||||
inviteeEmails: string[];
|
||||
organizationRoleSlug: string;
|
||||
|
@@ -314,11 +314,122 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findProjectMembershipsByUserIds = async (orgId: string, userIds: string[]) => {
|
||||
try {
|
||||
const docs = await db
|
||||
.replicaNode()(TableName.ProjectMembership)
|
||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.whereIn(`${TableName.Users}.id`, userIds)
|
||||
.where(`${TableName.Project}.orgId`, orgId)
|
||||
.join<TUserEncryptionKeys>(
|
||||
TableName.UserEncryptionKey,
|
||||
`${TableName.UserEncryptionKey}.userId`,
|
||||
`${TableName.Users}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.ProjectUserMembershipRole,
|
||||
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
|
||||
`${TableName.ProjectMembership}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.ProjectRoles,
|
||||
`${TableName.ProjectUserMembershipRole}.customRoleId`,
|
||||
`${TableName.ProjectRoles}.id`
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ProjectMembership),
|
||||
db.ref("isGhost").withSchema(TableName.Users),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
|
||||
db.ref("firstName").withSchema(TableName.Users),
|
||||
db.ref("lastName").withSchema(TableName.Users),
|
||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
db.ref("role").withSchema(TableName.ProjectUserMembershipRole),
|
||||
db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("membershipRoleId"),
|
||||
db.ref("customRoleId").withSchema(TableName.ProjectUserMembershipRole),
|
||||
db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
|
||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||
db.ref("temporaryMode").withSchema(TableName.ProjectUserMembershipRole),
|
||||
db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole),
|
||||
db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole),
|
||||
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||
db.ref("id").as("projectId").withSchema(TableName.Project),
|
||||
db.ref("type").as("projectType").withSchema(TableName.Project)
|
||||
)
|
||||
.where({ isGhost: false });
|
||||
|
||||
const members = sqlNestRelationships({
|
||||
data: docs,
|
||||
parentMapper: ({
|
||||
email,
|
||||
firstName,
|
||||
username,
|
||||
lastName,
|
||||
publicKey,
|
||||
isGhost,
|
||||
id,
|
||||
projectId,
|
||||
projectName,
|
||||
projectType,
|
||||
userId
|
||||
}) => ({
|
||||
id,
|
||||
userId,
|
||||
projectId,
|
||||
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
|
||||
project: {
|
||||
id: projectId,
|
||||
name: projectName,
|
||||
type: projectType
|
||||
}
|
||||
}),
|
||||
key: "id",
|
||||
childrenMapper: [
|
||||
{
|
||||
label: "roles" as const,
|
||||
key: "membershipRoleId",
|
||||
mapper: ({
|
||||
role,
|
||||
customRoleId,
|
||||
customRoleName,
|
||||
customRoleSlug,
|
||||
membershipRoleId,
|
||||
temporaryRange,
|
||||
temporaryMode,
|
||||
temporaryAccessEndTime,
|
||||
temporaryAccessStartTime,
|
||||
isTemporary
|
||||
}) => ({
|
||||
id: membershipRoleId,
|
||||
role,
|
||||
customRoleId,
|
||||
customRoleName,
|
||||
customRoleSlug,
|
||||
temporaryRange,
|
||||
temporaryMode,
|
||||
temporaryAccessEndTime,
|
||||
temporaryAccessStartTime,
|
||||
isTemporary
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
return members;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find project memberships by user ids" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...projectMemberOrm,
|
||||
findAllProjectMembers,
|
||||
findProjectGhostUser,
|
||||
findMembershipsByUsername,
|
||||
findProjectMembershipsByUserId
|
||||
findProjectMembershipsByUserId,
|
||||
findProjectMembershipsByUserIds
|
||||
};
|
||||
};
|
||||
|
@@ -13,7 +13,7 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
import { TAzureAppConfigurationSyncWithCredentials } from "./azure-app-configuration-sync-types";
|
||||
|
||||
type TAzureAppConfigurationSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
|
@@ -12,7 +12,7 @@ import { SecretSyncError } from "../secret-sync-errors";
|
||||
import { GetAzureKeyVaultSecret, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault-sync-types";
|
||||
|
||||
type TAzureKeyVaultSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
|
@@ -704,10 +704,39 @@ export const superAdminServiceFactory = ({
|
||||
};
|
||||
|
||||
const deleteUser = async (userId: string) => {
|
||||
const superAdmins = await userDAL.find({
|
||||
superAdmin: true
|
||||
});
|
||||
|
||||
if (superAdmins.length === 1 && superAdmins[0].id === userId) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot delete the only server admin on this instance. Add another server admin to delete this user."
|
||||
});
|
||||
}
|
||||
|
||||
const user = await userDAL.deleteById(userId);
|
||||
return user;
|
||||
};
|
||||
|
||||
const deleteUsers = async (userIds: string[]) => {
|
||||
const superAdmins = await userDAL.find({
|
||||
superAdmin: true
|
||||
});
|
||||
|
||||
if (superAdmins.every((superAdmin) => userIds.includes(superAdmin.id))) {
|
||||
throw new BadRequestError({
|
||||
message: "Instance must have at least one server admin. Add another server admin to delete these users."
|
||||
});
|
||||
}
|
||||
|
||||
const users = await userDAL.delete({
|
||||
$in: {
|
||||
id: userIds
|
||||
}
|
||||
});
|
||||
return users;
|
||||
};
|
||||
|
||||
const deleteIdentitySuperAdminAccess = async (identityId: string, actorId: string) => {
|
||||
const identity = await identityDAL.findById(identityId);
|
||||
if (!identity) {
|
||||
@@ -730,6 +759,17 @@ export const superAdminServiceFactory = ({
|
||||
throw new NotFoundError({ name: "User", message: "User not found" });
|
||||
}
|
||||
|
||||
const superAdmins = await userDAL.find({
|
||||
superAdmin: true
|
||||
});
|
||||
|
||||
if (superAdmins.length === 1 && superAdmins[0].id === userId) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Cannot remove the only server admin on this instance. Add another server admin to remove status for this user."
|
||||
});
|
||||
}
|
||||
|
||||
const updatedUser = userDAL.updateById(userId, { superAdmin: false });
|
||||
|
||||
return updatedUser;
|
||||
@@ -913,6 +953,7 @@ export const superAdminServiceFactory = ({
|
||||
initializeAdminIntegrationConfigSync,
|
||||
initializeEnvConfigSync,
|
||||
getEnvOverrides,
|
||||
getEnvOverridesOrganized
|
||||
getEnvOverridesOrganized,
|
||||
deleteUsers
|
||||
};
|
||||
};
|
||||
|
BIN
company/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1.9 KiB |
@@ -5,7 +5,7 @@
|
||||
"light": "/logo/light.svg",
|
||||
"href": "https://infisical.com"
|
||||
},
|
||||
"favicon": "/favicon.png",
|
||||
"favicon": "/favicon.ico",
|
||||
"colors": {
|
||||
"primary": "#26272b",
|
||||
"light": "#97b31d",
|
||||
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Bulk Delete User Memberships"
|
||||
openapi: "DELETE /api/v2/organizations/{organizationId}/memberships"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v2/secret-scanning/data-sources/gitlab"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab/data-source-name/{dataSourceName}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List Resources"
|
||||
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}/resources"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List Scans"
|
||||
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}/scans"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v2/secret-scanning/data-sources/gitlab"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Scan Resource"
|
||||
openapi: "POST /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}/resources/{resourceId}/scan"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Scan"
|
||||
openapi: "POST /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}/scan"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v2/secret-scanning/data-sources/gitlab/{dataSourceId}"
|
||||
---
|
@@ -10,7 +10,7 @@
|
||||
"styling": {
|
||||
"codeblocks": "dark"
|
||||
},
|
||||
"favicon": "/favicon.png",
|
||||
"favicon": "/favicon.ico",
|
||||
"navigation": {
|
||||
"tabs": [
|
||||
{
|
||||
@@ -220,7 +220,8 @@
|
||||
"pages": [
|
||||
"documentation/platform/secret-scanning/overview",
|
||||
"documentation/platform/secret-scanning/bitbucket",
|
||||
"documentation/platform/secret-scanning/github"
|
||||
"documentation/platform/secret-scanning/github",
|
||||
"documentation/platform/secret-scanning/gitlab"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -904,6 +905,7 @@
|
||||
"api-reference/endpoints/organizations/memberships",
|
||||
"api-reference/endpoints/organizations/update-membership",
|
||||
"api-reference/endpoints/organizations/delete-membership",
|
||||
"api-reference/endpoints/organizations/bulk-delete-memberships",
|
||||
"api-reference/endpoints/organizations/list-identity-memberships",
|
||||
"api-reference/endpoints/organizations/workspaces"
|
||||
]
|
||||
@@ -1203,6 +1205,21 @@
|
||||
"api-reference/endpoints/secret-scanning/data-sources/github/scan",
|
||||
"api-reference/endpoints/secret-scanning/data-sources/github/scan-resource"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "GitLab",
|
||||
"pages": [
|
||||
"api-reference/endpoints/secret-scanning/data-sources/gitlab/list",
|
||||
"api-reference/endpoints/secret-scanning/data-sources/gitlab/get-by-id",
|
||||
"api-reference/endpoints/secret-scanning/data-sources/gitlab/get-by-name",
|
||||
"api-reference/endpoints/secret-scanning/data-sources/gitlab/list-resources",
|
||||
"api-reference/endpoints/secret-scanning/data-sources/gitlab/list-scans",
|
||||
"api-reference/endpoints/secret-scanning/data-sources/gitlab/create",
|
||||
"api-reference/endpoints/secret-scanning/data-sources/gitlab/update",
|
||||
"api-reference/endpoints/secret-scanning/data-sources/gitlab/delete",
|
||||
"api-reference/endpoints/secret-scanning/data-sources/gitlab/scan",
|
||||
"api-reference/endpoints/secret-scanning/data-sources/gitlab/scan-resource"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
103
docs/documentation/platform/secret-scanning/gitlab.mdx
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: "GitLab Secret Scanning"
|
||||
sidebarTitle: "GitLab"
|
||||
description: "Learn how to configure secret scanning for GitLab."
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Create a [GitLab Connection](/integrations/app-connections/gitlab) with Secret Scanning permissions
|
||||
|
||||
## Create a GitLab Data Source in Infisical
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
1. Navigate to your Secret Scanning Project's Dashboard and click the **Add Data Source** button.
|
||||

|
||||
|
||||
2. Select the **GitLab** option.
|
||||

|
||||
|
||||
3. Configure which workspace and repositories you would like to scan. Then click **Next**.
|
||||

|
||||
|
||||
- **GitLab Connection** - the connection that has access to the repositories you want to scan.
|
||||
- **Scope** - the GitLab scope to scan secrets in.
|
||||
- **Project** - scan an individual GitLab project.
|
||||
- **Group** - scan one or more projects belonging to a GitLab group.
|
||||
- **Scan Repositories** - when using **Group Scope**, select which repositories you would like to scan.
|
||||
- **All Repositories** - Infisical will scan all repositories associated with your connection.
|
||||
- **Select Repositories** - Infisical will scan the selected repositories.
|
||||
- **Auto-Scan Enabled** - whether Infisical should automatically perform a scan when a push is made to configured repositories.
|
||||
|
||||
4. Give your data source a name and description (optional). Then click **Next**.
|
||||

|
||||
|
||||
- **Name** - the name of the data source. Must be slug-friendly.
|
||||
- **Description** (optional) - a description of this data source.
|
||||
|
||||
5. Review your data source, then click **Create Data Source**.
|
||||

|
||||
|
||||
6. Your **GitLab Data Source** is now available and will begin a full scan if **Auto-Scan** is enabled.
|
||||

|
||||
|
||||
7. You can view repositories and scan results by clicking on your data source.
|
||||

|
||||
|
||||
8. In addition, you can review any findings from the **Findings Page**.
|
||||

|
||||
</Tab>
|
||||
<Tab title="API">
|
||||
To create a GitLab Data Source, make an API request to the [Create GitLab Data Source](/api-reference/endpoints/secret-scanning/data-sources/gitlab/create) API endpoint.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://us.infisical.com/api/v2/secret-scanning/data-sources/gitlab \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "my-gitlab-source",
|
||||
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"description": "my gitlab data source",
|
||||
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"isAutoScanEnabled": true,
|
||||
"config": {
|
||||
"scope": "project",
|
||||
"projectId": 123456789,
|
||||
"projectName": "my-group/my-project"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Sample response
|
||||
|
||||
```bash Response
|
||||
{
|
||||
"dataSource": {
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"externalId": "1234567890",
|
||||
"name": "my-gitlab-source",
|
||||
"description": "my gitlab data source",
|
||||
"isAutoScanEnabled": true,
|
||||
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"createdAt": "2023-11-07T05:31:56Z",
|
||||
"updatedAt": "2023-11-07T05:31:56Z",
|
||||
"type": "gitlab",
|
||||
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"connection": {
|
||||
"app": "gitlab",
|
||||
"name": "my-gitlab-app",
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
|
||||
},
|
||||
"config": {
|
||||
"scope": "project",
|
||||
"projectId": 123456789,
|
||||
"projectName": "my-group/my-project"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
BIN
docs/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
docs/favicon.png
Before Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 446 KiB |
After Width: | Height: | Size: 424 KiB |
After Width: | Height: | Size: 442 KiB |
After Width: | Height: | Size: 675 KiB |
Before Width: | Height: | Size: 531 KiB After Width: | Height: | Size: 531 KiB |
After Width: | Height: | Size: 618 KiB |
Before Width: | Height: | Size: 480 KiB After Width: | Height: | Size: 480 KiB |
After Width: | Height: | Size: 652 KiB |
Before Width: | Height: | Size: 426 KiB After Width: | Height: | Size: 426 KiB |
After Width: | Height: | Size: 621 KiB |
Before Width: | Height: | Size: 464 KiB After Width: | Height: | Size: 464 KiB |
BIN
docs/images/platform/secret-scanning/gitlab/step-2.png
Normal file
After Width: | Height: | Size: 557 KiB |
BIN
docs/images/platform/secret-scanning/gitlab/step-3.png
Normal file
After Width: | Height: | Size: 621 KiB |
BIN
docs/images/platform/secret-scanning/gitlab/step-4.png
Normal file
After Width: | Height: | Size: 586 KiB |
BIN
docs/images/platform/secret-scanning/gitlab/step-5.png
Normal file
After Width: | Height: | Size: 584 KiB |
BIN
docs/images/platform/secret-scanning/gitlab/step-6.png
Normal file
After Width: | Height: | Size: 935 KiB |
BIN
docs/images/platform/secret-scanning/gitlab/step-7.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/images/platform/secret-scanning/gitlab/step-8.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
@@ -3,7 +3,7 @@ title: "Azure App Configuration Connection"
|
||||
description: "Learn how to configure a Azure App Configuration Connection for Infisical."
|
||||
---
|
||||
|
||||
Infisical currently only supports one method for connecting to Azure, which is OAuth.
|
||||
Infisical currently only supports two methods for connecting to Azure, which are OAuth and Client Secrets.
|
||||
|
||||
<Accordion title="Self-Hosted Instance">
|
||||
Using the Azure App Configuration connection on a self-hosted instance of Infisical requires configuring an application in Azure
|
||||
@@ -58,6 +58,26 @@ Infisical currently only supports one method for connecting to Azure, which is O
|
||||
</Steps>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Client Secret Authentication">
|
||||
To use client secret authentication, ensure your Azure Service Principal has the required permissions and is connected to the Azure App Configuration resources you want to use.
|
||||
|
||||
**Prerequisites:**
|
||||
- Set up Azure and have an existing App Configuration instance.
|
||||
- The service principal must be connected to your target Azure App Configuration resource(s)
|
||||
|
||||
<Steps>
|
||||
<Step title="Assign API permissions to the service principal">
|
||||
|
||||
Configure the required API permissions for your App Registration to interact with Azure App Configuration:
|
||||
|
||||
#### Azure App Configuration permissions
|
||||
|
||||
Set the API permissions of your Azure service principal to include the following Azure App Configuration permissions: `KeyValue.Delete`, `KeyValue.Read`, and `KeyValue.Write`.
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
## Setup Azure Connection in Infisical
|
||||
|
||||
@@ -66,25 +86,35 @@ Infisical currently only supports one method for connecting to Azure, which is O
|
||||
Navigate to the **App Connections** tab on the **Organization Settings** page. 
|
||||
</Step>
|
||||
<Step title="Add Connection">
|
||||
Select the **Azure Connection** option from the connection options modal. 
|
||||
</Step>
|
||||
<Step title="Authorize Connection">
|
||||
You can optionally authenticate against a specific tenant by providing the Azure Tenant or Directory ID.
|
||||
<Step title="Add Connection">
|
||||
Select the **Azure Connection** option from the connection options modal. 
|
||||
</Step>
|
||||
<Step title="Create Connection">
|
||||
<Tabs>
|
||||
<Tab title="OAuth">
|
||||
<Step title="Authorize Connection">
|
||||
You can optionally authenticate against a specific tenant by providing the Azure Tenant or Directory ID.
|
||||
|
||||
Now select the **OAuth** method and click **Connect to Azure**.
|
||||
Now select the **OAuth** method and click **Connect to Azure**.
|
||||
|
||||

|
||||

|
||||
</Step>
|
||||
<Step title="Grant Access">
|
||||
You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted,
|
||||
you will redirect you back to Infisical's App Connections page. 
|
||||
</Step>
|
||||
</Tab>
|
||||
<Tab title="Client Secret">
|
||||
<Step title="Create Connection">
|
||||
Fill in the **Tenant ID**, **Client ID** and **Client Secret** fields with the Directory (Tenant) ID, Application (Client) ID and Client Secret you obtained in the previous step.
|
||||
|
||||
|
||||
|
||||
</Step>
|
||||
<Step title="Grant Access">
|
||||
You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted,
|
||||
you will redirect you back to Infisical's App Connections page. 
|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||

|
||||
</Step>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
Your **Azure App Configuration Connection** is now available for use. 
|
||||
</Step>
|
||||
</Steps>
|
||||
|
@@ -3,7 +3,7 @@ title: "Azure Client Secrets Connection"
|
||||
description: "Learn how to configure an Azure Client Secrets Connection for Infisical."
|
||||
---
|
||||
|
||||
Infisical currently only supports one method for connecting to Azure, which is OAuth.
|
||||
Infisical currently only supports two methods for connecting to Azure, which are OAuth and Client Secrets.
|
||||
|
||||
<Accordion title="Self-Hosted Instance">
|
||||
Using the Azure Client Secrets connection on a self-hosted instance of Infisical requires configuring an application in Azure
|
||||
|
@@ -3,7 +3,7 @@ title: "Azure DevOps Connection"
|
||||
description: "Learn how to configure an Azure DevOps Connection for Infisical."
|
||||
---
|
||||
|
||||
Infisical currently supports two methods for connecting to Azure DevOps, which are OAuth and Azure DevOps Personal Access Token.
|
||||
Infisical currently supports three methods for connecting to Azure DevOps, which are OAuth, Azure DevOps Personal Access Token and Client Secrets.
|
||||
|
||||
<Accordion title="Azure OAuth on a Self-Hosted Instance">
|
||||
Using the Azure DevOps <b>OAuth connection</b> on a self-hosted instance of Infisical requires configuring an application in Azure
|
||||
@@ -87,6 +87,32 @@ Infisical currently supports two methods for connecting to Azure DevOps, which a
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Client Secret Authentication">
|
||||
To use client secret authentication, ensure your Azure Service Principal has the required permissions and is connected to the Azure DevOps organization and projects you want to use.
|
||||
|
||||
**Prerequisites:**
|
||||
- Set up Azure and have an existing Azure DevOps organization.
|
||||
- The service principal must be connected to your target Azure DevOps organization and project(s)
|
||||
|
||||
<Steps>
|
||||
<Step title="Assign API permissions to the service principal">
|
||||
|
||||
Configure the required API permissions for your App Registration to interact with Azure DevOps:
|
||||
|
||||
#### Azure DevOps permissions
|
||||
|
||||
Set the API permissions of your Azure service principal to include the following Azure DevOps permissions:
|
||||
- Azure DevOps
|
||||
- `user_impersonation`
|
||||
- `vso.project_write`
|
||||
- `vso.variablegroups_manage`
|
||||
- `vso.variablegroups_write`
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
## Setup Azure Connection in Infisical
|
||||
|
||||
<Steps>
|
||||
@@ -129,6 +155,17 @@ Infisical currently supports two methods for connecting to Azure DevOps, which a
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
<Tab title="Client Secret">
|
||||
<Steps>
|
||||
<Step title="Create Connection">
|
||||
Fill in the **Tenant ID**, **Client ID**, **Client Secret** and **Organization Name** fields with the Directory (Tenant) ID, Application (Client) ID, Client Secret and the organization name you obtained in the previous step.
|
||||
<Tip>
|
||||
You can find the **Organization Name** on https://dev.azure.com/
|
||||
</Tip>
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
|
@@ -3,7 +3,7 @@ title: "Azure Key Vault Connection"
|
||||
description: "Learn how to configure a Azure Key Vault Connection for Infisical."
|
||||
---
|
||||
|
||||
Infisical currently only supports one method for connecting to Azure, which is OAuth.
|
||||
Infisical currently only supports two methods for connecting to Azure, which are OAuth and Client Secrets.
|
||||
|
||||
<Accordion title="Self-Hosted Instance">
|
||||
Using the Azure Key Vault connection on a self-hosted instance of Infisical requires configuring an application in Azure
|
||||
@@ -58,6 +58,27 @@ Infisical currently only supports one method for connecting to Azure, which is O
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Client Secret Authentication">
|
||||
To use client secret authentication, ensure your Azure Service Principal has the required permissions and is connected to the Azure Key Vault instances you want to use.
|
||||
|
||||
**Prerequisites:**
|
||||
- Set up Azure and have an existing Key Vault instance.
|
||||
- The service principal must be connected to your target Azure Key Vault instance(s)
|
||||
|
||||
<Steps>
|
||||
<Step title="Assign API permissions to the service principal">
|
||||
|
||||
Configure the required API permissions for your App Registration to interact with Azure Key Vault:
|
||||
|
||||
#### Azure Key Vault permissions
|
||||
|
||||
Set the API permissions of your Azure service principal to include `user_impersonation` for the Key Vault API.
|
||||

|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
## Setup Azure Connection in Infisical
|
||||
|
||||
<Steps>
|
||||
@@ -68,21 +89,37 @@ Infisical currently only supports one method for connecting to Azure, which is O
|
||||
<Step title="Add Connection">
|
||||
Select the **Azure Connection** option from the connection options modal. 
|
||||
</Step>
|
||||
<Step title="Authorize Connection">
|
||||
You can optionally authenticate against a specific tenant by providing the Azure Tenant or Directory ID.
|
||||
<Step title="Create Connection">
|
||||
<Tabs>
|
||||
<Tab title="OAuth">
|
||||
<Steps>
|
||||
<Step title="Authorize Connection">
|
||||
You can optionally authenticate against a specific tenant by providing the Azure Tenant or Directory ID.
|
||||
|
||||
Now select the **OAuth** method and click **Connect to Azure**.
|
||||
Now select the **OAuth** method and click **Connect to Azure**.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
</Step>
|
||||
<Step title="Grant Access">
|
||||
You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted,
|
||||
you will redirect you back to Infisical's App Connections page. 
|
||||
</Step>
|
||||
</Step>
|
||||
<Step title="Grant Access">
|
||||
You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted,
|
||||
you will redirect you back to Infisical's App Connections page. 
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
<Tab title="Client Secret">
|
||||
<Steps>
|
||||
<Step title="Create Connection">
|
||||
Fill in the **Tenant ID**, **Client ID**, **Client Secret** fields with the Directory (Tenant) ID, Application (Client) ID, Client Secret you obtained in the previous step.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
Your **Azure Key Vault Connection** is now available for use. 
|
||||
</Step>
|
||||
|
@@ -26,8 +26,22 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
|
||||
|
||||
Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/organization/app-connections/gitlab/oauth/callback`.
|
||||
|
||||

|
||||

|
||||
Depending on your use case, add one or more of the following scopes to your application:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Secret Sync">
|
||||
For Secret Syncs, your application will require the `api` scope:
|
||||
|
||||

|
||||

|
||||
</Tab>
|
||||
<Tab title="Secret Scanning">
|
||||
For Secret Scanning, your application will require the `api` and `read_repository` scopes:
|
||||
|
||||

|
||||

|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Tip>
|
||||
The domain you defined in the Redirect URI should be equivalent to the `SITE_URL` configured in your Infisical instance.
|
||||
@@ -96,16 +110,22 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
|
||||

|
||||
</Step>
|
||||
<Step title="Configure Token">
|
||||
<Tabs>
|
||||
<Tab title="Secret Sync">
|
||||
For Secret Syncs, your token will require the ability to access the API:
|
||||
Fill in the token details:
|
||||
- **Token name**: A descriptive name for the token (e.g., "connection-token")
|
||||
- **Expiration date**: Set an appropriate expiration date
|
||||
- **Select scopes**: Choose the **api** scope for full API access
|
||||
- **Select scopes**: Depending on your use case, add one or more of the following scopes:
|
||||
|
||||

|
||||
<Tabs>
|
||||
<Tab title="Secret Sync">
|
||||
For Secret Syncs, your token will require the `api` scope:
|
||||
|
||||

|
||||
</Tab>
|
||||
<Tab title="Secret Scanning">
|
||||
For Secret Scanning, your token will require the `api` and `read_repository` scopes:
|
||||
|
||||

|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Info>
|
||||
@@ -134,17 +154,22 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
|
||||

|
||||
</Step>
|
||||
<Step title="Configure Token">
|
||||
<Tabs>
|
||||
<Tab title="Secret Sync">
|
||||
For Secret Syncs, your token will require the ability to access the API and be at least an **Owner**:
|
||||
Fill in the token details:
|
||||
- **Token name**: A descriptive name for the token
|
||||
- **Expiration date**: Set an appropriate expiration date
|
||||
- **Select role**: Choose **Owner** or higher role
|
||||
- **Select scopes**: Choose the **api** scope for API access
|
||||
Fill in the token details:
|
||||
- **Token name**: A descriptive name for the token
|
||||
- **Expiration date**: Set an appropriate expiration date
|
||||
- **Select role and scopes**: Depending on your use case, add the required role and one or more of the following scopes:
|
||||
|
||||

|
||||
</Tab>
|
||||
<Tabs>
|
||||
<Tab title="Secret Sync">
|
||||
For Secret Syncs, your token will require the `api` scope and at least the **Owner** role:
|
||||
|
||||

|
||||
</Tab>
|
||||
<Tab title="Secret Scanning">
|
||||
For Secret Scanning, your token will require the `api` and `read_repository` scopes and the **Maintainer** role:
|
||||
|
||||

|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Info>
|
||||
|
Before Width: | Height: | Size: 422 KiB After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,272 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { MultiValue, SingleValue } from "react-select";
|
||||
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
TGitLabGroup,
|
||||
TGitLabProject,
|
||||
useGitLabConnectionListGroups,
|
||||
useGitLabConnectionListProjects
|
||||
} from "@app/hooks/api/appConnections/gitlab";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
import { GitLabDataSourceScope } from "@app/hooks/api/secretScanningV2/types/gitlab-data-source";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||
import { SecretScanningDataSourceConnectionField } from "../SecretScanningDataSourceConnectionField";
|
||||
|
||||
enum ScanMethod {
|
||||
AllProjects = "all-projects",
|
||||
SelectProjects = "select-projects"
|
||||
}
|
||||
|
||||
export const GitLabDataSourceConfigFields = () => {
|
||||
const { control, watch, setValue } = useFormContext<
|
||||
TSecretScanningDataSourceForm & {
|
||||
type: SecretScanningDataSource.GitLab;
|
||||
}
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ control, name: "connection.id" });
|
||||
const isUpdate = Boolean(watch("id"));
|
||||
|
||||
const scope = watch("config.scope");
|
||||
const groupName = watch("config.groupName");
|
||||
const includeProjects = watch("config.includeProjects");
|
||||
|
||||
const { data: projects, isPending: isProjectsPending } = useGitLabConnectionListProjects(
|
||||
connectionId,
|
||||
{ enabled: Boolean(connectionId) }
|
||||
);
|
||||
|
||||
const { data: groups, isPending: isGroupsPending } = useGitLabConnectionListGroups(connectionId, {
|
||||
enabled: Boolean(connectionId) && scope === GitLabDataSourceScope.Group
|
||||
});
|
||||
|
||||
const scanMethod =
|
||||
!includeProjects || includeProjects.includes("*")
|
||||
? ScanMethod.AllProjects
|
||||
: ScanMethod.SelectProjects;
|
||||
|
||||
useEffect(() => {
|
||||
if (!includeProjects) {
|
||||
setValue("config.includeProjects", ["*"]);
|
||||
}
|
||||
}, [includeProjects]);
|
||||
|
||||
const clearAllFields = () => {
|
||||
setValue("config.includeProjects", []);
|
||||
setValue("config.projectName", "");
|
||||
setValue("config.groupName", "");
|
||||
// @ts-expect-error rhf doesn't like this but we need to reset
|
||||
setValue("config.projectId", undefined);
|
||||
// @ts-expect-error rhf doesn't like this but we need to reset
|
||||
setValue("config.groupId", undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretScanningDataSourceConnectionField isUpdate={isUpdate} onChange={clearAllFields} />
|
||||
<Controller
|
||||
name="config.scope"
|
||||
control={control}
|
||||
defaultValue={GitLabDataSourceScope.Project}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Scope"
|
||||
helperText={isUpdate ? "Cannot be updated" : undefined}
|
||||
tooltipText={
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>Specify the GitLab scope scanning should be performed at:</p>
|
||||
<ul className="flex list-disc flex-col gap-3 pl-4">
|
||||
<li>
|
||||
<p className="text-mineshaft-300">
|
||||
<span className="font-medium text-bunker-200">Project</span>: Scan an
|
||||
individual GitLab project.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p className="text-mineshaft-300">
|
||||
<span className="font-medium text-bunker-200">Group</span>: Scan one or more
|
||||
projects belonging to a GitLab group.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(v) => {
|
||||
onChange(v);
|
||||
clearAllFields();
|
||||
}}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
isDisabled={isUpdate}
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(GitLabDataSourceScope).map((method) => {
|
||||
return (
|
||||
<SelectItem className="capitalize" value={method} key={method}>
|
||||
{method}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{scope === GitLabDataSourceScope.Project ? (
|
||||
<Controller
|
||||
name="config.projectId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Project"
|
||||
helperText={
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={<>Ensure that your connection has the correct permissions.</>}
|
||||
>
|
||||
<div>
|
||||
<span>Don't see the project you're looking for?</span>{" "}
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isProjectsPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={projects?.find((project) => value === Number.parseInt(project.id, 10))}
|
||||
onChange={(newValue) => {
|
||||
const project = newValue as SingleValue<TGitLabProject>;
|
||||
|
||||
onChange(project ? Number.parseInt(project.id, 10) : null);
|
||||
setValue("config.projectName", project?.name ?? "");
|
||||
}}
|
||||
options={projects}
|
||||
placeholder="Select project..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Controller
|
||||
name="config.groupId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Group"
|
||||
helperText={
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={<>Ensure that your connection has the correct permissions.</>}
|
||||
>
|
||||
<div>
|
||||
<span>Don't see the group you're looking for?</span>{" "}
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isGroupsPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={groups?.find((group) => value === Number.parseInt(group.id, 10))}
|
||||
onChange={(newValue) => {
|
||||
const group = newValue as SingleValue<TGitLabGroup>;
|
||||
|
||||
onChange(group ? Number.parseInt(group.id, 10) : null);
|
||||
setValue("config.groupName", group?.name ?? "");
|
||||
setValue("config.includeProjects", ["*"]);
|
||||
}}
|
||||
options={groups}
|
||||
placeholder="Select group..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormControl label="Scan Projects">
|
||||
<Select
|
||||
value={scanMethod}
|
||||
onValueChange={(val) => {
|
||||
setValue("config.includeProjects", val === ScanMethod.AllProjects ? ["*"] : []);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isDisabled={!connectionId}
|
||||
>
|
||||
{Object.values(ScanMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem className="capitalize" value={method} key={method}>
|
||||
{method.replace("-", " ")}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{scanMethod === ScanMethod.SelectProjects && (
|
||||
<Controller
|
||||
name="config.includeProjects"
|
||||
defaultValue={["*"]}
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Include Projects"
|
||||
helperText={
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={<>Ensure that your connection has the correct permissions.</>}
|
||||
>
|
||||
<div>
|
||||
<span>Don't see the project you're looking for?</span>{" "}
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={isProjectsPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId || !groupName}
|
||||
isMulti
|
||||
value={projects?.filter((project) => value.includes(project.name))}
|
||||
onChange={(newValue) => {
|
||||
onChange(
|
||||
newValue
|
||||
? (newValue as MultiValue<TGitLabProject>).map((p) => p.name)
|
||||
: null
|
||||
);
|
||||
}}
|
||||
options={projects?.filter((project) => project.name.startsWith(groupName))}
|
||||
placeholder="Select projects..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@@ -7,10 +7,12 @@ import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||
import { BitbucketDataSourceConfigFields } from "./BitbucketDataSourceConfigFields";
|
||||
import { GitHubDataSourceConfigFields } from "./GitHubDataSourceConfigFields";
|
||||
import { GitLabDataSourceConfigFields } from "./GitLabDataSourceConfigFields";
|
||||
|
||||
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
|
||||
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields,
|
||||
[SecretScanningDataSource.Bitbucket]: BitbucketDataSourceConfigFields
|
||||
[SecretScanningDataSource.Bitbucket]: BitbucketDataSourceConfigFields,
|
||||
[SecretScanningDataSource.GitLab]: GitLabDataSourceConfigFields
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceConfigFields = () => {
|
||||
|