Compare commits
62 Commits
daniel/fix
...
update-aws
Author | SHA1 | Date | |
---|---|---|---|
2e256e4282 | |||
dcd21883d1 | |||
205442bff5 | |||
e8d19eb823 | |||
5d30215ea7 | |||
29fedfdde5 | |||
b5317d1d75 | |||
86c145301e | |||
6446311b6d | |||
3e80f1907c | |||
79e62eec25 | |||
c41730c5fb | |||
f0b9d3c816 | |||
ea393d144a | |||
c4c0f86598 | |||
1f7617d132 | |||
c95680b95d | |||
18f1f93b5f | |||
70ea761375 | |||
5b4790ee78 | |||
5ab2a6bb5d | |||
dcac85fe6c | |||
2f07471404 | |||
137fd5ef07 | |||
883c7835a1 | |||
0366e58a5b | |||
9f6dca23db | |||
18e733c71f | |||
f0a95808e7 | |||
90a0d0f744 | |||
7f9c9be2c8 | |||
070982081c | |||
f462c3f85d | |||
8683693103 | |||
737fffcceb | |||
ffac24ce75 | |||
c505c5877f | |||
b59fa14bb6 | |||
d4bf8a33dc | |||
0eb36d7e35 | |||
ae2da0066a | |||
6566393e21 | |||
1d7da56b40 | |||
af245b1f16 | |||
c17df7e951 | |||
4d4953e95a | |||
43e0d400f9 | |||
02a2309953 | |||
f1587d8375 | |||
198e74cd88 | |||
8ed0a1de84 | |||
c305ddd463 | |||
27cb686216 | |||
e201d77a8f | |||
470d7cca6a | |||
8e3918ada3 | |||
bd54054bc3 | |||
cfe51d4a52 | |||
9cdd7380df | |||
07d491acd1 | |||
3276853427 | |||
2b8220a71b |
@ -107,6 +107,10 @@ INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
|
||||
INF_APP_CONNECTION_GITHUB_APP_SLUG=
|
||||
INF_APP_CONNECTION_GITHUB_APP_ID=
|
||||
|
||||
#gitlab app connection
|
||||
INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID=
|
||||
INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET=
|
||||
|
||||
#github radar app connection
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID=
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET=
|
||||
|
@ -51,11 +51,18 @@ jobs:
|
||||
--from-literal=ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 \
|
||||
--from-literal=SITE_URL=http://localhost:8080
|
||||
|
||||
- name: Create bootstrap secret
|
||||
run: |
|
||||
kubectl create secret generic infisical-bootstrap-credentials \
|
||||
--namespace infisical-standalone-postgres \
|
||||
--from-literal=INFISICAL_ADMIN_EMAIL=admin@example.com \
|
||||
--from-literal=INFISICAL_ADMIN_PASSWORD=admin-password
|
||||
|
||||
- name: Run chart-testing (install)
|
||||
run: |
|
||||
ct install \
|
||||
--config ct.yaml \
|
||||
--charts helm-charts/infisical-standalone-postgres \
|
||||
--helm-extra-args="--timeout=300s" \
|
||||
--helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres" \
|
||||
--helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres --set infisical.autoBootstrap.enabled=true" \
|
||||
--namespace infisical-standalone-postgres
|
||||
|
67
backend/package-lock.json
generated
@ -30,6 +30,7 @@
|
||||
"@fastify/static": "^7.0.4",
|
||||
"@fastify/swagger": "^8.14.0",
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@gitbeaker/rest": "^42.5.0",
|
||||
"@google-cloud/kms": "^4.5.0",
|
||||
"@infisical/quic": "^1.0.8",
|
||||
"@node-saml/passport-saml": "^5.0.1",
|
||||
@ -7807,6 +7808,48 @@
|
||||
"p-limit": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@gitbeaker/core": {
|
||||
"version": "42.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@gitbeaker/core/-/core-42.5.0.tgz",
|
||||
"integrity": "sha512-rMWpOPaZi1iLiifnOIoVO57p2EmQQdfIwP4txqNyMvG4WjYP5Ez0U7jRD9Nra41x6K5kTPBZkuQcAdxVWRJcEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@gitbeaker/requester-utils": "^42.5.0",
|
||||
"qs": "^6.12.2",
|
||||
"xcase": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@gitbeaker/requester-utils": {
|
||||
"version": "42.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-42.5.0.tgz",
|
||||
"integrity": "sha512-HLdLS9LPBMVQumvroQg/4qkphLDtwDB+ygEsrD2u4oYCMUtXV4V1xaVqU4yTXjbTJ5sItOtdB43vYRkBcgueBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch-browser": "^2.2.6",
|
||||
"qs": "^6.12.2",
|
||||
"rate-limiter-flexible": "^4.0.1",
|
||||
"xcase": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@gitbeaker/rest": {
|
||||
"version": "42.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@gitbeaker/rest/-/rest-42.5.0.tgz",
|
||||
"integrity": "sha512-oC5cM6jS7aFOp0luTw5mWSRuMgdxwHRLZQ/aWkI+ETMfsprR/HyxsXfljlMY/XJ/fRxTbRJiodR5Axf66WjO3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@gitbeaker/core": "^42.5.0",
|
||||
"@gitbeaker/requester-utils": "^42.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/kms": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-4.5.0.tgz",
|
||||
@ -24628,6 +24671,18 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch-browser": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/picomatch-browser/-/picomatch-browser-2.2.6.tgz",
|
||||
"integrity": "sha512-0ypsOQt9D4e3hziV8O4elD9uN0z/jtUEfxVRtNaAAtXIyUx9m/SzlO020i8YNL2aL/E6blOvvHQcin6HZlFy/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||
@ -25562,6 +25617,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/rate-limiter-flexible": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-4.0.1.tgz",
|
||||
"integrity": "sha512-2/dGHpDFpeA0+755oUkW+EKyklqLS9lu0go9pDsbhqQjZcxfRyJ6LA4JI0+HAdZ2bemD/oOjUeZQB2lCZqXQfQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||
@ -31039,6 +31100,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xcase": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xcase/-/xcase-2.0.1.tgz",
|
||||
"integrity": "sha512-UmFXIPU+9Eg3E9m/728Bii0lAIuoc+6nbrNUKaRPJOFp91ih44qqGlWtxMB6kXFrRD6po+86ksHM5XHCfk6iPw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml-crypto": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.0.1.tgz",
|
||||
|
@ -149,6 +149,7 @@
|
||||
"@fastify/static": "^7.0.4",
|
||||
"@fastify/swagger": "^8.14.0",
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@gitbeaker/rest": "^42.5.0",
|
||||
"@google-cloud/kms": "^4.5.0",
|
||||
"@infisical/quic": "^1.0.8",
|
||||
"@node-saml/passport-saml": "^5.0.1",
|
||||
|
@ -0,0 +1,21 @@
|
||||
export function providerSpecificPayload(url: string) {
|
||||
const { hostname } = new URL(url);
|
||||
|
||||
const payload: Record<string, string> = {};
|
||||
|
||||
switch (hostname) {
|
||||
case "http-intake.logs.datadoghq.com":
|
||||
case "http-intake.logs.us3.datadoghq.com":
|
||||
case "http-intake.logs.us5.datadoghq.com":
|
||||
case "http-intake.logs.datadoghq.eu":
|
||||
case "http-intake.logs.ap1.datadoghq.com":
|
||||
case "http-intake.logs.ddog-gov.com":
|
||||
payload.ddsource = "infisical";
|
||||
payload.service = "audit-logs";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
@ -13,6 +13,7 @@ import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
||||
import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal";
|
||||
import { providerSpecificPayload } from "./audit-log-stream-fns";
|
||||
import { LogStreamHeaders, TAuditLogStreamServiceFactory } from "./audit-log-stream-types";
|
||||
|
||||
type TAuditLogStreamServiceFactoryDep = {
|
||||
@ -69,10 +70,11 @@ export const auditLogStreamServiceFactory = ({
|
||||
headers.forEach(({ key, value }) => {
|
||||
streamHeaders[key] = value;
|
||||
});
|
||||
|
||||
await request
|
||||
.post(
|
||||
url,
|
||||
{ ping: "ok" },
|
||||
{ ...providerSpecificPayload(url), ping: "ok" },
|
||||
{
|
||||
headers: streamHeaders,
|
||||
// request timeout
|
||||
@ -137,7 +139,7 @@ export const auditLogStreamServiceFactory = ({
|
||||
await request
|
||||
.post(
|
||||
url || logStream.url,
|
||||
{ ping: "ok" },
|
||||
{ ...providerSpecificPayload(url || logStream.url), ping: "ok" },
|
||||
{
|
||||
headers: streamHeaders,
|
||||
// request timeout
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { RawAxiosRequestHeaders } from "axios";
|
||||
import { AxiosError, RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TAuditLogStreamDALFactory } from "../audit-log-stream/audit-log-stream-dal";
|
||||
import { providerSpecificPayload } from "../audit-log-stream/audit-log-stream-fns";
|
||||
import { LogStreamHeaders } from "../audit-log-stream/audit-log-stream-types";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TAuditLogDALFactory } from "./audit-log-dal";
|
||||
@ -128,13 +130,29 @@ export const auditLogQueueServiceFactory = async ({
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
return request.post(url, auditLog, {
|
||||
headers,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
});
|
||||
try {
|
||||
logger.info(`Streaming audit log [url=${url}] for org [orgId=${orgId}]`);
|
||||
const response = await request.post(
|
||||
url,
|
||||
{ ...providerSpecificPayload(url), ...auditLog },
|
||||
{
|
||||
headers,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
);
|
||||
logger.info(
|
||||
`Successfully streamed audit log [url=${url}] for org [orgId=${orgId}] [response=${JSON.stringify(response.data)}]`
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to stream audit log [url=${url}] for org [orgId=${orgId}] [error=${(error as AxiosError).message}]`
|
||||
);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
@ -218,13 +236,29 @@ export const auditLogQueueServiceFactory = async ({
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
return request.post(url, auditLog, {
|
||||
headers,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
});
|
||||
try {
|
||||
logger.info(`Streaming audit log [url=${url}] for org [orgId=${orgId}]`);
|
||||
const response = await request.post(
|
||||
url,
|
||||
{ ...providerSpecificPayload(url), ...auditLog },
|
||||
{
|
||||
headers,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
);
|
||||
logger.info(
|
||||
`Successfully streamed audit log [url=${url}] for org [orgId=${orgId}] [response=${JSON.stringify(response.data)}]`
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to stream audit log [url=${url}] for org [orgId=${orgId}] [error=${(error as AxiosError).message}]`
|
||||
);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
@ -2228,6 +2228,12 @@ export const AppConnections = {
|
||||
},
|
||||
FLYIO: {
|
||||
accessToken: "The Access Token used to access fly.io."
|
||||
},
|
||||
GITLAB: {
|
||||
instanceUrl: "The GitLab instance URL to connect with.",
|
||||
accessToken: "The Access Token used to access GitLab.",
|
||||
code: "The OAuth code to use to connect with GitLab.",
|
||||
accessTokenType: "The type of token used to connect with GitLab."
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -2402,6 +2408,17 @@ export const SecretSyncs = {
|
||||
FLYIO: {
|
||||
appId: "The ID of the Fly.io app to sync secrets to."
|
||||
},
|
||||
GITLAB: {
|
||||
projectId: "The GitLab Project ID to sync secrets to.",
|
||||
projectName: "The GitLab Project Name to sync secrets to.",
|
||||
groupId: "The GitLab Group ID to sync secrets to.",
|
||||
groupName: "The GitLab Group Name to sync secrets to.",
|
||||
scope: "The GitLab scope that secrets should be synced to. (default: project)",
|
||||
targetEnvironment: "The GitLab environment scope that secrets should be synced to. (default: *)",
|
||||
shouldProtectSecrets: "Whether variables should be protected",
|
||||
shouldMaskSecrets: "Whether variables should be masked in logs",
|
||||
shouldHideSecrets: "Whether variables should be hidden"
|
||||
},
|
||||
CLOUDFLARE_PAGES: {
|
||||
projectName: "The name of the Cloudflare Pages project to sync secrets to.",
|
||||
environment: "The environment of the Cloudflare Pages project to sync secrets to."
|
||||
|
@ -247,6 +247,10 @@ const envSchema = z
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_ID: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET: zpStr(z.string().optional()),
|
||||
|
||||
// gitlab oauth
|
||||
INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
|
||||
// gcp app
|
||||
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()),
|
||||
|
||||
|
@ -35,6 +35,10 @@ import {
|
||||
CamundaConnectionListItemSchema,
|
||||
SanitizedCamundaConnectionSchema
|
||||
} from "@app/services/app-connection/camunda";
|
||||
import {
|
||||
CloudflareConnectionListItemSchema,
|
||||
SanitizedCloudflareConnectionSchema
|
||||
} from "@app/services/app-connection/cloudflare/cloudflare-connection-schema";
|
||||
import {
|
||||
DatabricksConnectionListItemSchema,
|
||||
SanitizedDatabricksConnectionSchema
|
||||
@ -46,6 +50,7 @@ import {
|
||||
GitHubRadarConnectionListItemSchema,
|
||||
SanitizedGitHubRadarConnectionSchema
|
||||
} from "@app/services/app-connection/github-radar";
|
||||
import { GitLabConnectionListItemSchema, SanitizedGitLabConnectionSchema } from "@app/services/app-connection/gitlab";
|
||||
import {
|
||||
HCVaultConnectionListItemSchema,
|
||||
SanitizedHCVaultConnectionSchema
|
||||
@ -80,10 +85,6 @@ import {
|
||||
WindmillConnectionListItemSchema
|
||||
} from "@app/services/app-connection/windmill";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import {
|
||||
CloudflareConnectionListItemSchema,
|
||||
SanitizedCloudflareConnectionSchema
|
||||
} from "@app/services/app-connection/cloudflare/cloudflare-connection-schema";
|
||||
|
||||
// can't use discriminated due to multiple schemas for certain apps
|
||||
const SanitizedAppConnectionSchema = z.union([
|
||||
@ -114,6 +115,7 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedHerokuConnectionSchema.options,
|
||||
...SanitizedRenderConnectionSchema.options,
|
||||
...SanitizedFlyioConnectionSchema.options,
|
||||
...SanitizedGitLabConnectionSchema.options,
|
||||
...SanitizedCloudflareConnectionSchema.options
|
||||
]);
|
||||
|
||||
@ -145,6 +147,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
HerokuConnectionListItemSchema,
|
||||
RenderConnectionListItemSchema,
|
||||
FlyioConnectionListItemSchema,
|
||||
GitLabConnectionListItemSchema,
|
||||
CloudflareConnectionListItemSchema
|
||||
]);
|
||||
|
||||
|
@ -0,0 +1,90 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateGitLabConnectionSchema,
|
||||
SanitizedGitLabConnectionSchema,
|
||||
TGitLabGroup,
|
||||
TGitLabProject,
|
||||
UpdateGitLabConnectionSchema
|
||||
} from "@app/services/app-connection/gitlab";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerGitLabConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.GitLab,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedGitLabConnectionSchema,
|
||||
createSchema: CreateGitLabConnectionSchema,
|
||||
updateSchema: UpdateGitLabConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/projects`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const projects: TGitLabProject[] = await server.services.appConnection.gitlab.listProjects(
|
||||
connectionId,
|
||||
req.permission
|
||||
);
|
||||
|
||||
return projects;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/groups`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const groups: TGitLabGroup[] = await server.services.appConnection.gitlab.listGroups(
|
||||
connectionId,
|
||||
req.permission
|
||||
);
|
||||
|
||||
return groups;
|
||||
}
|
||||
});
|
||||
};
|
@ -10,11 +10,13 @@ import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secre
|
||||
import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router";
|
||||
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
|
||||
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
|
||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||
import { registerGitHubRadarConnectionRouter } from "./github-radar-connection-router";
|
||||
import { registerGitLabConnectionRouter } from "./gitlab-connection-router";
|
||||
import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router";
|
||||
import { registerHerokuConnectionRouter } from "./heroku-connection-router";
|
||||
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
||||
@ -27,7 +29,6 @@ import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
|
||||
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
|
||||
import { registerVercelConnectionRouter } from "./vercel-connection-router";
|
||||
import { registerWindmillConnectionRouter } from "./windmill-connection-router";
|
||||
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
||||
|
||||
export * from "./app-connection-router";
|
||||
|
||||
@ -60,5 +61,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.Heroku]: registerHerokuConnectionRouter,
|
||||
[AppConnection.Render]: registerRenderConnectionRouter,
|
||||
[AppConnection.Flyio]: registerFlyioConnectionRouter,
|
||||
[AppConnection.GitLab]: registerGitLabConnectionRouter,
|
||||
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter
|
||||
};
|
||||
|
@ -0,0 +1,13 @@
|
||||
import { CreateGitLabSyncSchema, GitLabSyncSchema, UpdateGitLabSyncSchema } from "@app/services/secret-sync/gitlab";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerGitLabSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.GitLab,
|
||||
server,
|
||||
responseSchema: GitLabSyncSchema,
|
||||
createSchema: CreateGitLabSyncSchema,
|
||||
updateSchema: UpdateGitLabSyncSchema
|
||||
});
|
@ -13,6 +13,7 @@ import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
||||
import { registerFlyioSyncRouter } from "./flyio-sync-router";
|
||||
import { registerGcpSyncRouter } from "./gcp-sync-router";
|
||||
import { registerGitHubSyncRouter } from "./github-sync-router";
|
||||
import { registerGitLabSyncRouter } from "./gitlab-sync-router";
|
||||
import { registerHCVaultSyncRouter } from "./hc-vault-sync-router";
|
||||
import { registerHerokuSyncRouter } from "./heroku-sync-router";
|
||||
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
||||
@ -45,5 +46,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.Heroku]: registerHerokuSyncRouter,
|
||||
[SecretSync.Render]: registerRenderSyncRouter,
|
||||
[SecretSync.Flyio]: registerFlyioSyncRouter,
|
||||
[SecretSync.GitLab]: registerGitLabSyncRouter,
|
||||
[SecretSync.CloudflarePages]: registerCloudflarePagesSyncRouter
|
||||
};
|
||||
|
@ -22,10 +22,15 @@ import {
|
||||
import { AzureDevOpsSyncListItemSchema, AzureDevOpsSyncSchema } from "@app/services/secret-sync/azure-devops";
|
||||
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
|
||||
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
||||
import {
|
||||
CloudflarePagesSyncListItemSchema,
|
||||
CloudflarePagesSyncSchema
|
||||
} from "@app/services/secret-sync/cloudflare-pages/cloudflare-pages-schema";
|
||||
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
|
||||
import { FlyioSyncListItemSchema, FlyioSyncSchema } from "@app/services/secret-sync/flyio";
|
||||
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
||||
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
||||
import { GitLabSyncListItemSchema, GitLabSyncSchema } from "@app/services/secret-sync/gitlab";
|
||||
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
|
||||
import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret-sync/heroku";
|
||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||
@ -34,10 +39,6 @@ import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/se
|
||||
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
||||
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
||||
import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill";
|
||||
import {
|
||||
CloudflarePagesSyncListItemSchema,
|
||||
CloudflarePagesSyncSchema
|
||||
} from "@app/services/secret-sync/cloudflare-pages/cloudflare-pages-schema";
|
||||
|
||||
const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
AwsParameterStoreSyncSchema,
|
||||
@ -60,6 +61,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
HerokuSyncSchema,
|
||||
RenderSyncSchema,
|
||||
FlyioSyncSchema,
|
||||
GitLabSyncSchema,
|
||||
CloudflarePagesSyncSchema
|
||||
]);
|
||||
|
||||
@ -84,6 +86,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
HerokuSyncListItemSchema,
|
||||
RenderSyncListItemSchema,
|
||||
FlyioSyncListItemSchema,
|
||||
GitLabSyncListItemSchema,
|
||||
CloudflarePagesSyncListItemSchema
|
||||
]);
|
||||
|
||||
|
@ -26,6 +26,7 @@ export enum AppConnection {
|
||||
Heroku = "heroku",
|
||||
Render = "render",
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab",
|
||||
Cloudflare = "cloudflare"
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,11 @@ import {
|
||||
validateAzureKeyVaultConnectionCredentials
|
||||
} from "./azure-key-vault";
|
||||
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
|
||||
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
|
||||
import {
|
||||
getCloudflareConnectionListItem,
|
||||
validateCloudflareConnectionCredentials
|
||||
} from "./cloudflare/cloudflare-connection-fns";
|
||||
import {
|
||||
DatabricksConnectionMethod,
|
||||
getDatabricksConnectionListItem,
|
||||
@ -64,6 +69,7 @@ import {
|
||||
GitHubRadarConnectionMethod,
|
||||
validateGitHubRadarConnectionCredentials
|
||||
} from "./github-radar";
|
||||
import { getGitLabConnectionListItem, GitLabConnectionMethod, validateGitLabConnectionCredentials } from "./gitlab";
|
||||
import {
|
||||
getHCVaultConnectionListItem,
|
||||
HCVaultConnectionMethod,
|
||||
@ -99,11 +105,6 @@ import {
|
||||
validateWindmillConnectionCredentials,
|
||||
WindmillConnectionMethod
|
||||
} from "./windmill";
|
||||
import {
|
||||
getCloudflareConnectionListItem,
|
||||
validateCloudflareConnectionCredentials
|
||||
} from "./cloudflare/cloudflare-connection-fns";
|
||||
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
|
||||
|
||||
export const listAppConnectionOptions = () => {
|
||||
return [
|
||||
@ -134,6 +135,7 @@ export const listAppConnectionOptions = () => {
|
||||
getHerokuConnectionListItem(),
|
||||
getRenderConnectionListItem(),
|
||||
getFlyioConnectionListItem(),
|
||||
getGitLabConnectionListItem(),
|
||||
getCloudflareConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
@ -213,6 +215,7 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Heroku]: validateHerokuConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
@ -230,6 +233,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case GitHubConnectionMethod.OAuth:
|
||||
case AzureDevOpsConnectionMethod.OAuth:
|
||||
case HerokuConnectionMethod.OAuth:
|
||||
case GitLabConnectionMethod.OAuth:
|
||||
return "OAuth";
|
||||
case HerokuConnectionMethod.AuthToken:
|
||||
return "Auth Token";
|
||||
@ -327,6 +331,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Heroku]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Render]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.GitLab]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
|
@ -28,6 +28,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Heroku]: "Heroku",
|
||||
[AppConnection.Render]: "Render",
|
||||
[AppConnection.Flyio]: "Fly.io",
|
||||
[AppConnection.GitLab]: "GitLab",
|
||||
[AppConnection.Cloudflare]: "Cloudflare"
|
||||
};
|
||||
|
||||
@ -59,5 +60,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Heroku]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Render]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.GitLab]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
@ -58,6 +58,8 @@ import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||
import { githubConnectionService } from "./github/github-connection-service";
|
||||
import { ValidateGitHubRadarConnectionCredentialsSchema } from "./github-radar";
|
||||
import { ValidateGitLabConnectionCredentialsSchema } from "./gitlab";
|
||||
import { gitlabConnectionService } from "./gitlab/gitlab-connection-service";
|
||||
import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault";
|
||||
import { hcVaultConnectionService } from "./hc-vault/hc-vault-connection-service";
|
||||
import { ValidateHerokuConnectionCredentialsSchema } from "./heroku";
|
||||
@ -116,6 +118,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Heroku]: ValidateHerokuConnectionCredentialsSchema,
|
||||
[AppConnection.Render]: ValidateRenderConnectionCredentialsSchema,
|
||||
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
|
||||
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
|
||||
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
@ -524,7 +527,8 @@ export const appConnectionServiceFactory = ({
|
||||
onepass: onePassConnectionService(connectAppConnectionById),
|
||||
heroku: herokuConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
render: renderConnectionService(connectAppConnectionById),
|
||||
cloudflare: cloudflareConnectionService(connectAppConnectionById),
|
||||
flyio: flyioConnectionService(connectAppConnectionById)
|
||||
flyio: flyioConnectionService(connectAppConnectionById),
|
||||
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
cloudflare: cloudflareConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
@ -62,6 +62,12 @@ import {
|
||||
TCamundaConnectionInput,
|
||||
TValidateCamundaConnectionCredentialsSchema
|
||||
} from "./camunda";
|
||||
import {
|
||||
TCloudflareConnection,
|
||||
TCloudflareConnectionConfig,
|
||||
TCloudflareConnectionInput,
|
||||
TValidateCloudflareConnectionCredentialsSchema
|
||||
} from "./cloudflare/cloudflare-connection-types";
|
||||
import {
|
||||
TDatabricksConnection,
|
||||
TDatabricksConnectionConfig,
|
||||
@ -92,6 +98,12 @@ import {
|
||||
TGitHubRadarConnectionInput,
|
||||
TValidateGitHubRadarConnectionCredentialsSchema
|
||||
} from "./github-radar";
|
||||
import {
|
||||
TGitLabConnection,
|
||||
TGitLabConnectionConfig,
|
||||
TGitLabConnectionInput,
|
||||
TValidateGitLabConnectionCredentialsSchema
|
||||
} from "./gitlab";
|
||||
import {
|
||||
THCVaultConnection,
|
||||
THCVaultConnectionConfig,
|
||||
@ -153,12 +165,6 @@ import {
|
||||
TWindmillConnectionConfig,
|
||||
TWindmillConnectionInput
|
||||
} from "./windmill";
|
||||
import {
|
||||
TCloudflareConnection,
|
||||
TCloudflareConnectionConfig,
|
||||
TCloudflareConnectionInput,
|
||||
TValidateCloudflareConnectionCredentialsSchema
|
||||
} from "./cloudflare/cloudflare-connection-types";
|
||||
|
||||
export type TAppConnection = { id: string } & (
|
||||
| TAwsConnection
|
||||
@ -188,6 +194,7 @@ export type TAppConnection = { id: string } & (
|
||||
| THerokuConnection
|
||||
| TRenderConnection
|
||||
| TFlyioConnection
|
||||
| TGitLabConnection
|
||||
| TCloudflareConnection
|
||||
);
|
||||
|
||||
@ -223,6 +230,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| THerokuConnectionInput
|
||||
| TRenderConnectionInput
|
||||
| TFlyioConnectionInput
|
||||
| TGitLabConnectionInput
|
||||
| TCloudflareConnectionInput
|
||||
);
|
||||
|
||||
@ -266,6 +274,7 @@ export type TAppConnectionConfig =
|
||||
| THerokuConnectionConfig
|
||||
| TRenderConnectionConfig
|
||||
| TFlyioConnectionConfig
|
||||
| TGitLabConnectionConfig
|
||||
| TCloudflareConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
@ -296,6 +305,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateHerokuConnectionCredentialsSchema
|
||||
| TValidateRenderConnectionCredentialsSchema
|
||||
| TValidateFlyioConnectionCredentialsSchema
|
||||
| TValidateGitLabConnectionCredentialsSchema
|
||||
| TValidateCloudflareConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
|
@ -0,0 +1,9 @@
|
||||
export enum GitLabConnectionMethod {
|
||||
OAuth = "oauth",
|
||||
AccessToken = "access-token"
|
||||
}
|
||||
|
||||
export enum GitLabAccessTokenType {
|
||||
Project = "project",
|
||||
Personal = "personal"
|
||||
}
|
@ -0,0 +1,351 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { GitbeakerRequestError, Gitlab } from "@gitbeaker/rest";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "../app-connection-dal";
|
||||
import { GitLabAccessTokenType, GitLabConnectionMethod } from "./gitlab-connection-enums";
|
||||
import { TGitLabConnection, TGitLabConnectionConfig, TGitLabGroup, TGitLabProject } from "./gitlab-connection-types";
|
||||
|
||||
interface GitLabOAuthTokenResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
created_at: number;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
export const getGitLabConnectionListItem = () => {
|
||||
const { INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID } = getConfig();
|
||||
|
||||
return {
|
||||
name: "GitLab" as const,
|
||||
app: AppConnection.GitLab as const,
|
||||
methods: Object.values(GitLabConnectionMethod) as [
|
||||
GitLabConnectionMethod.AccessToken,
|
||||
GitLabConnectionMethod.OAuth
|
||||
],
|
||||
oauthClientId: INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID
|
||||
};
|
||||
};
|
||||
|
||||
export const getGitLabInstanceUrl = async (instanceUrl?: string) => {
|
||||
const gitLabInstanceUrl = instanceUrl ? removeTrailingSlash(instanceUrl) : IntegrationUrls.GITLAB_URL;
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(gitLabInstanceUrl);
|
||||
|
||||
return gitLabInstanceUrl;
|
||||
};
|
||||
|
||||
export const getGitLabClient = async (accessToken: string, instanceUrl?: string, isOAuth = false) => {
|
||||
const host = await getGitLabInstanceUrl(instanceUrl);
|
||||
|
||||
const client = new Gitlab<true>({
|
||||
host,
|
||||
...(isOAuth ? { oauthToken: accessToken } : { token: accessToken }),
|
||||
camelize: true
|
||||
});
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
export const refreshGitLabToken = async (
|
||||
refreshToken: string,
|
||||
appId: string,
|
||||
orgId: string,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">,
|
||||
instanceUrl?: string
|
||||
): Promise<string> => {
|
||||
const { INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET, SITE_URL } =
|
||||
getConfig();
|
||||
if (!INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET || !INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID || !SITE_URL) {
|
||||
throw new InternalServerError({
|
||||
message: `GitLab environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
const payload = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_id: INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET,
|
||||
redirect_uri: `${SITE_URL}/organization/app-connections/gitlab/oauth/callback`
|
||||
});
|
||||
|
||||
try {
|
||||
const url = await getGitLabInstanceUrl(instanceUrl);
|
||||
const { data } = await request.post<GitLabOAuthTokenResponse>(`${url}/oauth/token`, payload.toString(), {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
const expiresAt = new Date(Date.now() + data.expires_in * 1000 - 600000);
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: {
|
||||
instanceUrl,
|
||||
tokenType: data.token_type,
|
||||
createdAt: new Date(data.created_at * 1000).toISOString(),
|
||||
refreshToken: data.refresh_token,
|
||||
accessToken: data.access_token,
|
||||
expiresAt
|
||||
},
|
||||
orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(appId, { encryptedCredentials });
|
||||
return data.access_token;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to refresh GitLab token: ${error.message}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to refresh GitLab token"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const exchangeGitLabOAuthCode = async (
|
||||
code: string,
|
||||
instanceUrl?: string
|
||||
): Promise<GitLabOAuthTokenResponse> => {
|
||||
const { INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET, SITE_URL } =
|
||||
getConfig();
|
||||
if (!INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET || !INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID || !SITE_URL) {
|
||||
throw new InternalServerError({
|
||||
message: `GitLab environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_id: INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET,
|
||||
redirect_uri: `${SITE_URL}/organization/app-connections/gitlab/oauth/callback`
|
||||
});
|
||||
const url = await getGitLabInstanceUrl(instanceUrl);
|
||||
|
||||
const response = await request.post<GitLabOAuthTokenResponse>(`${url}/oauth/token`, payload.toString(), {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.data) {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to exchange OAuth code: Empty response"
|
||||
});
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to exchange OAuth code: ${error.message}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to exchange OAuth code"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateGitLabConnectionCredentials = async (config: TGitLabConnectionConfig) => {
|
||||
const { credentials: inputCredentials, method } = config;
|
||||
|
||||
let accessToken: string;
|
||||
let oauthData: GitLabOAuthTokenResponse | null = null;
|
||||
|
||||
if (method === GitLabConnectionMethod.OAuth && "code" in inputCredentials) {
|
||||
oauthData = await exchangeGitLabOAuthCode(inputCredentials.code, inputCredentials.instanceUrl);
|
||||
accessToken = oauthData.access_token;
|
||||
} else if (method === GitLabConnectionMethod.AccessToken && "accessToken" in inputCredentials) {
|
||||
accessToken = inputCredentials.accessToken;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid credentials for the selected connection method"
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const client = await getGitLabClient(
|
||||
accessToken,
|
||||
inputCredentials.instanceUrl,
|
||||
method === GitLabConnectionMethod.OAuth
|
||||
);
|
||||
await client.Users.showCurrentUser();
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Error validating GitLab connection credentials");
|
||||
|
||||
if (error instanceof GitbeakerRequestError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}`
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${(error as Error)?.message || "verify credentials"}`
|
||||
});
|
||||
}
|
||||
|
||||
if (method === GitLabConnectionMethod.OAuth && oauthData) {
|
||||
return {
|
||||
accessToken,
|
||||
instanceUrl: inputCredentials.instanceUrl,
|
||||
refreshToken: oauthData.refresh_token,
|
||||
expiresAt: new Date(Date.now() + oauthData.expires_in * 1000 - 60000),
|
||||
tokenType: oauthData.token_type,
|
||||
createdAt: new Date(oauthData.created_at * 1000)
|
||||
};
|
||||
}
|
||||
|
||||
return inputCredentials;
|
||||
};
|
||||
|
||||
export const listGitLabProjects = async ({
|
||||
appConnection,
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}: {
|
||||
appConnection: TGitLabConnection;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}): Promise<TGitLabProject[]> => {
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = await getGitLabClient(
|
||||
accessToken,
|
||||
appConnection.credentials.instanceUrl,
|
||||
appConnection.method === GitLabConnectionMethod.OAuth
|
||||
);
|
||||
const projects = await client.Projects.all({
|
||||
archived: false,
|
||||
includePendingDelete: false,
|
||||
membership: true,
|
||||
includeHidden: false,
|
||||
imported: false
|
||||
});
|
||||
|
||||
return projects.map((project) => ({
|
||||
name: project.pathWithNamespace,
|
||||
id: project.id.toString()
|
||||
}));
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof GitbeakerRequestError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to fetch GitLab projects: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}`
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof InternalServerError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new InternalServerError({
|
||||
message: "Unable to fetch GitLab projects"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const listGitLabGroups = async ({
|
||||
appConnection,
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}: {
|
||||
appConnection: TGitLabConnection;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}): Promise<TGitLabGroup[]> => {
|
||||
let { accessToken } = appConnection.credentials;
|
||||
|
||||
if (
|
||||
appConnection.method === GitLabConnectionMethod.AccessToken &&
|
||||
appConnection.credentials.accessTokenType === GitLabAccessTokenType.Project
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = await getGitLabClient(
|
||||
accessToken,
|
||||
appConnection.credentials.instanceUrl,
|
||||
appConnection.method === GitLabConnectionMethod.OAuth
|
||||
);
|
||||
|
||||
const groups = await client.Groups.all({
|
||||
orderBy: "name",
|
||||
sort: "asc",
|
||||
minAccessLevel: 50
|
||||
});
|
||||
|
||||
return groups.map((group) => ({
|
||||
id: group.id.toString(),
|
||||
name: group.name
|
||||
}));
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof GitbeakerRequestError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to fetch GitLab groups: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}`
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof InternalServerError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new InternalServerError({
|
||||
message: "Unable to fetch GitLab groups"
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,138 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { GitLabAccessTokenType, GitLabConnectionMethod } from "./gitlab-connection-enums";
|
||||
|
||||
export const GitLabConnectionAccessTokenCredentialsSchema = z.object({
|
||||
accessToken: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Access Token required")
|
||||
.describe(AppConnections.CREDENTIALS.GITLAB.accessToken),
|
||||
instanceUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.url("Invalid Instance URL")
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.GITLAB.instanceUrl),
|
||||
accessTokenType: z.nativeEnum(GitLabAccessTokenType).describe(AppConnections.CREDENTIALS.GITLAB.accessTokenType)
|
||||
});
|
||||
|
||||
export const GitLabConnectionOAuthCredentialsSchema = z.object({
|
||||
code: z.string().trim().min(1, "OAuth code required").describe(AppConnections.CREDENTIALS.GITLAB.code),
|
||||
instanceUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.url("Invalid Instance URL")
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.GITLAB.instanceUrl)
|
||||
});
|
||||
|
||||
export const GitLabConnectionOAuthOutputCredentialsSchema = z.object({
|
||||
accessToken: z.string().trim(),
|
||||
refreshToken: z.string().trim(),
|
||||
expiresAt: z.date(),
|
||||
tokenType: z.string().optional().default("bearer"),
|
||||
createdAt: z.string().optional(),
|
||||
instanceUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.url("Invalid Instance URL")
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.GITLAB.instanceUrl)
|
||||
});
|
||||
|
||||
export const GitLabConnectionRefreshTokenCredentialsSchema = z.object({
|
||||
refreshToken: z.string().trim().min(1, "Refresh token required"),
|
||||
instanceUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.url("Invalid Instance URL")
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.GITLAB.instanceUrl)
|
||||
});
|
||||
|
||||
const BaseGitLabConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.GitLab)
|
||||
});
|
||||
|
||||
export const GitLabConnectionSchema = z.intersection(
|
||||
BaseGitLabConnectionSchema,
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(GitLabConnectionMethod.AccessToken),
|
||||
credentials: GitLabConnectionAccessTokenCredentialsSchema
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(GitLabConnectionMethod.OAuth),
|
||||
credentials: GitLabConnectionOAuthOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
export const SanitizedGitLabConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseGitLabConnectionSchema.extend({
|
||||
method: z.literal(GitLabConnectionMethod.AccessToken),
|
||||
credentials: GitLabConnectionAccessTokenCredentialsSchema.pick({
|
||||
instanceUrl: true,
|
||||
accessTokenType: true
|
||||
})
|
||||
}),
|
||||
BaseGitLabConnectionSchema.extend({
|
||||
method: z.literal(GitLabConnectionMethod.OAuth),
|
||||
credentials: GitLabConnectionOAuthOutputCredentialsSchema.pick({
|
||||
instanceUrl: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateGitLabConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(GitLabConnectionMethod.AccessToken).describe(AppConnections.CREATE(AppConnection.GitLab).method),
|
||||
credentials: GitLabConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.GitLab).credentials
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(GitLabConnectionMethod.OAuth).describe(AppConnections.CREATE(AppConnection.GitLab).method),
|
||||
credentials: z
|
||||
.union([
|
||||
GitLabConnectionOAuthCredentialsSchema,
|
||||
GitLabConnectionRefreshTokenCredentialsSchema,
|
||||
GitLabConnectionOAuthOutputCredentialsSchema
|
||||
])
|
||||
.describe(AppConnections.CREATE(AppConnection.GitLab).credentials)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateGitLabConnectionSchema = ValidateGitLabConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.GitLab)
|
||||
);
|
||||
|
||||
export const UpdateGitLabConnectionSchema = z
|
||||
.object({
|
||||
credentials: z
|
||||
.union([
|
||||
GitLabConnectionAccessTokenCredentialsSchema,
|
||||
GitLabConnectionOAuthOutputCredentialsSchema,
|
||||
GitLabConnectionRefreshTokenCredentialsSchema,
|
||||
GitLabConnectionOAuthCredentialsSchema
|
||||
])
|
||||
.optional()
|
||||
.describe(AppConnections.UPDATE(AppConnection.GitLab).credentials)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitLab));
|
||||
|
||||
export const GitLabConnectionListItemSchema = z.object({
|
||||
name: z.literal("GitLab"),
|
||||
app: z.literal(AppConnection.GitLab),
|
||||
methods: z.nativeEnum(GitLabConnectionMethod).array(),
|
||||
oauthClientId: z.string().optional()
|
||||
});
|
@ -0,0 +1,47 @@
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "../app-connection-dal";
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listGitLabGroups, listGitLabProjects } from "./gitlab-connection-fns";
|
||||
import { TGitLabConnection } from "./gitlab-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TGitLabConnection>;
|
||||
|
||||
export const gitlabConnectionService = (
|
||||
getAppConnection: TGetAppConnectionFunc,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
try {
|
||||
const appConnection = await getAppConnection(AppConnection.GitLab, connectionId, actor);
|
||||
const projects = await listGitLabProjects({ appConnection, appConnectionDAL, kmsService });
|
||||
return projects;
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to establish connection with GitLab for app ${connectionId}`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const listGroups = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
try {
|
||||
const appConnection = await getAppConnection(AppConnection.GitLab, connectionId, actor);
|
||||
const groups = await listGitLabGroups({ appConnection, appConnectionDAL, kmsService });
|
||||
return groups;
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to establish connection with GitLab for app ${connectionId}`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listProjects,
|
||||
listGroups
|
||||
};
|
||||
};
|
@ -0,0 +1,56 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateGitLabConnectionSchema,
|
||||
GitLabConnectionSchema,
|
||||
ValidateGitLabConnectionCredentialsSchema
|
||||
} from "./gitlab-connection-schemas";
|
||||
|
||||
export type TGitLabConnection = z.infer<typeof GitLabConnectionSchema>;
|
||||
|
||||
export type TGitLabConnectionInput = z.infer<typeof CreateGitLabConnectionSchema> & {
|
||||
app: AppConnection.GitLab;
|
||||
};
|
||||
|
||||
export type TValidateGitLabConnectionCredentialsSchema = typeof ValidateGitLabConnectionCredentialsSchema;
|
||||
|
||||
export type TGitLabConnectionConfig = DiscriminativePick<TGitLabConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TGitLabProject = {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TGitLabAccessTokenCredentials = {
|
||||
accessToken: string;
|
||||
instanceUrl: string;
|
||||
};
|
||||
|
||||
export type TGitLabOAuthCredentials = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresAt: Date;
|
||||
tokenType?: string;
|
||||
createdAt?: Date;
|
||||
instanceUrl: string;
|
||||
};
|
||||
|
||||
export type TGitLabOAuthCodeCredentials = {
|
||||
code: string;
|
||||
instanceUrl: string;
|
||||
};
|
||||
|
||||
export type TGitLabRefreshTokenCredentials = {
|
||||
refreshToken: string;
|
||||
instanceUrl: string;
|
||||
};
|
||||
|
||||
export interface TGitLabGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
4
backend/src/services/app-connection/gitlab/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./gitlab-connection-enums";
|
||||
export * from "./gitlab-connection-fns";
|
||||
export * from "./gitlab-connection-schemas";
|
||||
export * from "./gitlab-connection-types";
|
@ -307,7 +307,6 @@ export const AwsParameterStoreSyncFns = {
|
||||
awsParameterStoreSecretsRecord,
|
||||
Boolean(syncOptions.tags?.length || syncOptions.syncSecretMetadataAsTags)
|
||||
);
|
||||
const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []);
|
||||
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value, secretMetadata }] = entry;
|
||||
@ -342,13 +341,13 @@ export const AwsParameterStoreSyncFns = {
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldManageTags) {
|
||||
if ((syncOptions.tags !== undefined || syncOptions.syncSecretMetadataAsTags) && shouldManageTags) {
|
||||
const { tagsToAdd, tagKeysToRemove } = processParameterTags({
|
||||
syncTagsRecord: {
|
||||
// configured sync tags take preference over secret metadata
|
||||
...(syncOptions.syncSecretMetadataAsTags &&
|
||||
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
|
||||
...syncTagsRecord
|
||||
...(syncOptions.tags && Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []))
|
||||
},
|
||||
awsTagsRecord: awsParameterStoreTagsRecord[key] ?? {}
|
||||
});
|
||||
|
@ -366,37 +366,39 @@ export const AwsSecretsManagerSyncFns = {
|
||||
}
|
||||
}
|
||||
|
||||
const { tagsToAdd, tagKeysToRemove } = processTags({
|
||||
syncTagsRecord: {
|
||||
// configured sync tags take preference over secret metadata
|
||||
...(syncOptions.syncSecretMetadataAsTags &&
|
||||
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
|
||||
...syncTagsRecord
|
||||
},
|
||||
awsTagsRecord: Object.fromEntries(
|
||||
awsDescriptionsRecord[key]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
|
||||
)
|
||||
});
|
||||
if (syncOptions.tags !== undefined || syncOptions.syncSecretMetadataAsTags) {
|
||||
const { tagsToAdd, tagKeysToRemove } = processTags({
|
||||
syncTagsRecord: {
|
||||
// configured sync tags take preference over secret metadata
|
||||
...(syncOptions.syncSecretMetadataAsTags &&
|
||||
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
|
||||
...(syncOptions.tags !== undefined && syncTagsRecord)
|
||||
},
|
||||
awsTagsRecord: Object.fromEntries(
|
||||
awsDescriptionsRecord[key]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
|
||||
)
|
||||
});
|
||||
|
||||
if (tagsToAdd.length) {
|
||||
try {
|
||||
await addTags(client, key, tagsToAdd);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
if (tagsToAdd.length) {
|
||||
try {
|
||||
await addTags(client, key, tagsToAdd);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tagKeysToRemove.length) {
|
||||
try {
|
||||
await removeTags(client, key, tagKeysToRemove);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
if (tagKeysToRemove.length) {
|
||||
try {
|
||||
await removeTags(client, key, tagKeysToRemove);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -439,32 +441,34 @@ export const AwsSecretsManagerSyncFns = {
|
||||
});
|
||||
}
|
||||
|
||||
const { tagsToAdd, tagKeysToRemove } = processTags({
|
||||
syncTagsRecord,
|
||||
awsTagsRecord: Object.fromEntries(
|
||||
awsDescriptionsRecord[destinationConfig.secretName]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
|
||||
)
|
||||
});
|
||||
if (syncOptions.tags !== undefined) {
|
||||
const { tagsToAdd, tagKeysToRemove } = processTags({
|
||||
syncTagsRecord,
|
||||
awsTagsRecord: Object.fromEntries(
|
||||
awsDescriptionsRecord[destinationConfig.secretName]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
|
||||
)
|
||||
});
|
||||
|
||||
if (tagsToAdd.length) {
|
||||
try {
|
||||
await addTags(client, destinationConfig.secretName, tagsToAdd);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: destinationConfig.secretName
|
||||
});
|
||||
if (tagsToAdd.length) {
|
||||
try {
|
||||
await addTags(client, destinationConfig.secretName, tagsToAdd);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: destinationConfig.secretName
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tagKeysToRemove.length) {
|
||||
try {
|
||||
await removeTags(client, destinationConfig.secretName, tagKeysToRemove);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: destinationConfig.secretName
|
||||
});
|
||||
if (tagKeysToRemove.length) {
|
||||
try {
|
||||
await removeTags(client, destinationConfig.secretName, tagKeysToRemove);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: destinationConfig.secretName
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const GITLAB_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "GitLab",
|
||||
destination: SecretSync.GitLab,
|
||||
connection: AppConnection.GitLab,
|
||||
canImportSecrets: false
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export enum GitLabSyncScope {
|
||||
Project = "project",
|
||||
Group = "group"
|
||||
}
|
452
backend/src/services/secret-sync/gitlab/gitlab-sync-fns.ts
Normal file
@ -0,0 +1,452 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { GitbeakerRequestError } from "@gitbeaker/rest";
|
||||
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import {
|
||||
getGitLabClient,
|
||||
GitLabConnectionMethod,
|
||||
refreshGitLabToken,
|
||||
TGitLabConnection
|
||||
} from "@app/services/app-connection/gitlab";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TGitLabSyncWithCredentials, TGitLabVariable } from "@app/services/secret-sync/gitlab/gitlab-sync-types";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
|
||||
import { GitLabSyncScope } from "./gitlab-sync-enums";
|
||||
|
||||
interface TGitLabVariablePayload {
|
||||
key?: string;
|
||||
value: string;
|
||||
variable_type?: "env_var" | "file";
|
||||
environment_scope?: string;
|
||||
protected?: boolean;
|
||||
masked?: boolean;
|
||||
masked_and_hidden?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface TGitLabVariableCreate extends TGitLabVariablePayload {
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface TGitLabVariableUpdate extends Omit<TGitLabVariablePayload, "key"> {}
|
||||
|
||||
type TGitLabSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
const getValidAccessToken = async (
|
||||
connection: TGitLabConnection,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
): Promise<string> => {
|
||||
if (
|
||||
connection.method === GitLabConnectionMethod.OAuth &&
|
||||
connection.credentials.refreshToken &&
|
||||
new Date(connection.credentials.expiresAt) < new Date()
|
||||
) {
|
||||
const accessToken = await refreshGitLabToken(
|
||||
connection.credentials.refreshToken,
|
||||
connection.id,
|
||||
connection.orgId,
|
||||
appConnectionDAL,
|
||||
kmsService,
|
||||
connection.credentials.instanceUrl
|
||||
);
|
||||
return accessToken;
|
||||
}
|
||||
return connection.credentials.accessToken;
|
||||
};
|
||||
|
||||
const getGitLabVariables = async ({
|
||||
accessToken,
|
||||
connection,
|
||||
scope,
|
||||
resourceId,
|
||||
targetEnvironment
|
||||
}: {
|
||||
accessToken: string;
|
||||
connection: TGitLabConnection;
|
||||
scope: GitLabSyncScope;
|
||||
resourceId: string;
|
||||
targetEnvironment?: string;
|
||||
}): Promise<TGitLabVariable[]> => {
|
||||
try {
|
||||
const client = await getGitLabClient(
|
||||
accessToken,
|
||||
connection.credentials.instanceUrl,
|
||||
connection.method === GitLabConnectionMethod.OAuth
|
||||
);
|
||||
|
||||
let variables: TGitLabVariable[] = [];
|
||||
|
||||
if (scope === GitLabSyncScope.Project) {
|
||||
variables = await client.ProjectVariables.all(resourceId);
|
||||
} else {
|
||||
variables = await client.GroupVariables.all(resourceId);
|
||||
}
|
||||
|
||||
if (targetEnvironment) {
|
||||
variables = variables.filter((v) => v.environmentScope === targetEnvironment);
|
||||
}
|
||||
|
||||
return variables;
|
||||
} catch (error) {
|
||||
if (error instanceof GitbeakerRequestError) {
|
||||
throw new SecretSyncError({
|
||||
error: new Error(
|
||||
`Failed to fetch variables: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}`
|
||||
)
|
||||
});
|
||||
}
|
||||
throw new SecretSyncError({
|
||||
error
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createGitLabVariable = async ({
|
||||
accessToken,
|
||||
connection,
|
||||
scope,
|
||||
resourceId,
|
||||
variable
|
||||
}: {
|
||||
accessToken: string;
|
||||
connection: TGitLabConnection;
|
||||
scope: GitLabSyncScope;
|
||||
resourceId: string;
|
||||
variable: TGitLabVariableCreate;
|
||||
}): Promise<void> => {
|
||||
try {
|
||||
const client = await getGitLabClient(
|
||||
accessToken,
|
||||
connection.credentials.instanceUrl,
|
||||
connection.method === GitLabConnectionMethod.OAuth
|
||||
);
|
||||
|
||||
const payload = {
|
||||
key: variable.key,
|
||||
value: variable.value,
|
||||
variableType: "env_var",
|
||||
environmentScope: variable.environment_scope || "*",
|
||||
protected: variable.protected || false,
|
||||
masked: variable.masked || false,
|
||||
masked_and_hidden: variable.masked_and_hidden || false,
|
||||
raw: false
|
||||
};
|
||||
|
||||
if (scope === GitLabSyncScope.Project) {
|
||||
await client.ProjectVariables.create(resourceId, payload.key, payload.value, {
|
||||
variableType: "env_var",
|
||||
environmentScope: payload.environmentScope,
|
||||
protected: payload.protected,
|
||||
masked: payload.masked,
|
||||
masked_and_hidden: payload.masked_and_hidden,
|
||||
raw: false
|
||||
});
|
||||
} else {
|
||||
await client.GroupVariables.create(resourceId, payload.key, payload.value, {
|
||||
variableType: "env_var",
|
||||
environmentScope: payload.environmentScope,
|
||||
protected: payload.protected,
|
||||
masked: payload.masked,
|
||||
...(payload.masked_and_hidden && { masked_and_hidden: payload.masked_and_hidden }),
|
||||
raw: false
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof GitbeakerRequestError) {
|
||||
throw new SecretSyncError({
|
||||
error: new Error(
|
||||
`Failed to create variable: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}`
|
||||
),
|
||||
secretKey: variable.key
|
||||
});
|
||||
}
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: variable.key
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateGitLabVariable = async ({
|
||||
accessToken,
|
||||
connection,
|
||||
scope,
|
||||
resourceId,
|
||||
key,
|
||||
variable,
|
||||
targetEnvironment
|
||||
}: {
|
||||
accessToken: string;
|
||||
connection: TGitLabConnection;
|
||||
scope: GitLabSyncScope;
|
||||
resourceId: string;
|
||||
key: string;
|
||||
variable: TGitLabVariableUpdate;
|
||||
targetEnvironment?: string;
|
||||
}): Promise<void> => {
|
||||
try {
|
||||
const client = await getGitLabClient(
|
||||
accessToken,
|
||||
connection.credentials.instanceUrl,
|
||||
connection.method === GitLabConnectionMethod.OAuth
|
||||
);
|
||||
|
||||
const options = {
|
||||
...(variable.environment_scope && { environmentScope: variable.environment_scope }),
|
||||
...(variable.protected !== undefined && { protected: variable.protected }),
|
||||
...(variable.masked !== undefined && { masked: variable.masked })
|
||||
};
|
||||
|
||||
if (targetEnvironment) {
|
||||
options.environmentScope = targetEnvironment;
|
||||
}
|
||||
|
||||
if (scope === GitLabSyncScope.Project) {
|
||||
await client.ProjectVariables.edit(resourceId, key, variable.value, {
|
||||
...options,
|
||||
filter: { environment_scope: targetEnvironment || "*" }
|
||||
});
|
||||
} else {
|
||||
await client.GroupVariables.edit(resourceId, key, variable.value, {
|
||||
...options,
|
||||
filter: { environment_scope: targetEnvironment || "*" }
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof GitbeakerRequestError) {
|
||||
throw new SecretSyncError({
|
||||
error: new Error(
|
||||
`Failed to update variable: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}`
|
||||
),
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteGitLabVariable = async ({
|
||||
accessToken,
|
||||
connection,
|
||||
scope,
|
||||
resourceId,
|
||||
key,
|
||||
targetEnvironment,
|
||||
allVariables
|
||||
}: {
|
||||
accessToken: string;
|
||||
connection: TGitLabConnection;
|
||||
scope: GitLabSyncScope;
|
||||
resourceId: string;
|
||||
key: string;
|
||||
targetEnvironment?: string;
|
||||
allVariables?: TGitLabVariable[];
|
||||
}): Promise<void> => {
|
||||
if (allVariables && !allVariables.find((v) => v.key === key)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const client = await getGitLabClient(
|
||||
accessToken,
|
||||
connection.credentials.instanceUrl,
|
||||
connection.method === GitLabConnectionMethod.OAuth
|
||||
);
|
||||
|
||||
const options: { filter?: { environment_scope: string } } = {};
|
||||
if (targetEnvironment) {
|
||||
options.filter = { environment_scope: targetEnvironment || "*" };
|
||||
}
|
||||
|
||||
if (scope === GitLabSyncScope.Project) {
|
||||
await client.ProjectVariables.remove(resourceId, key, options);
|
||||
} else {
|
||||
await client.GroupVariables.remove(resourceId, key);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof GitbeakerRequestError) {
|
||||
throw new SecretSyncError({
|
||||
error: new Error(
|
||||
`Failed to delete variable: ${error.message ?? "Unknown error"}${error.cause?.description && error.message !== "Unauthorized" ? `. Cause: ${error.cause.description}` : ""}`
|
||||
),
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const GitLabSyncFns = {
|
||||
syncSecrets: async (
|
||||
secretSync: TGitLabSyncWithCredentials,
|
||||
secretMap: TSecretMap,
|
||||
{ appConnectionDAL, kmsService }: TGitLabSyncFactoryDeps
|
||||
): Promise<void> => {
|
||||
const { connection, environment, destinationConfig } = secretSync;
|
||||
const { scope, targetEnvironment } = destinationConfig;
|
||||
|
||||
const resourceId = scope === GitLabSyncScope.Project ? destinationConfig.projectId : destinationConfig.groupId;
|
||||
|
||||
const accessToken = await getValidAccessToken(connection, appConnectionDAL, kmsService);
|
||||
|
||||
try {
|
||||
const currentVariables = await getGitLabVariables({
|
||||
accessToken,
|
||||
connection,
|
||||
scope,
|
||||
resourceId,
|
||||
targetEnvironment
|
||||
});
|
||||
|
||||
const currentVariableMap = new Map(currentVariables.map((v) => [v.key, v]));
|
||||
|
||||
for (const [key, { value }] of Object.entries(secretMap)) {
|
||||
if (value?.length < 8 && destinationConfig.shouldMaskSecrets) {
|
||||
throw new SecretSyncError({
|
||||
message: `Secret ${key} is too short to be masked. GitLab requires a minimum of 8 characters for masked secrets.`,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
try {
|
||||
const existingVariable = currentVariableMap.get(key);
|
||||
|
||||
if (existingVariable) {
|
||||
if (
|
||||
existingVariable.value !== value ||
|
||||
existingVariable.environmentScope !== targetEnvironment ||
|
||||
existingVariable.protected !== destinationConfig.shouldProtectSecrets ||
|
||||
existingVariable.masked !== destinationConfig.shouldMaskSecrets
|
||||
) {
|
||||
await updateGitLabVariable({
|
||||
accessToken,
|
||||
connection,
|
||||
scope,
|
||||
resourceId,
|
||||
key,
|
||||
variable: {
|
||||
value,
|
||||
environment_scope: targetEnvironment,
|
||||
protected: destinationConfig.shouldProtectSecrets,
|
||||
masked: destinationConfig.shouldMaskSecrets || existingVariable.hidden
|
||||
},
|
||||
targetEnvironment
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await createGitLabVariable({
|
||||
accessToken,
|
||||
connection,
|
||||
scope,
|
||||
resourceId,
|
||||
variable: {
|
||||
key,
|
||||
value,
|
||||
variable_type: "env_var",
|
||||
environment_scope: targetEnvironment || "*",
|
||||
protected: destinationConfig.shouldProtectSecrets || false,
|
||||
masked: destinationConfig.shouldMaskSecrets || false,
|
||||
masked_and_hidden: destinationConfig.shouldHideSecrets || false
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!secretSync.syncOptions.disableSecretDeletion) {
|
||||
for (const variable of currentVariables) {
|
||||
try {
|
||||
const shouldDelete =
|
||||
matchesSchema(variable.key, environment?.slug || "", secretSync.syncOptions.keySchema) &&
|
||||
!(variable.key in secretMap);
|
||||
|
||||
if (shouldDelete) {
|
||||
await deleteGitLabVariable({
|
||||
accessToken,
|
||||
connection,
|
||||
scope,
|
||||
resourceId,
|
||||
key: variable.key,
|
||||
targetEnvironment
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: variable.key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SecretSyncError) {
|
||||
throw error;
|
||||
}
|
||||
throw new SecretSyncError({
|
||||
message: "Failed to sync secrets",
|
||||
error
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removeSecrets: async (
|
||||
secretSync: TGitLabSyncWithCredentials,
|
||||
secretMap: TSecretMap,
|
||||
{ appConnectionDAL, kmsService }: TGitLabSyncFactoryDeps
|
||||
): Promise<void> => {
|
||||
const { connection, destinationConfig } = secretSync;
|
||||
const { scope, targetEnvironment } = destinationConfig;
|
||||
|
||||
const resourceId = scope === GitLabSyncScope.Project ? destinationConfig.projectId : destinationConfig.groupId;
|
||||
|
||||
const accessToken = await getValidAccessToken(connection, appConnectionDAL, kmsService);
|
||||
|
||||
const allVariables = await getGitLabVariables({
|
||||
accessToken,
|
||||
connection,
|
||||
scope,
|
||||
resourceId,
|
||||
targetEnvironment
|
||||
});
|
||||
|
||||
for (const key of Object.keys(secretMap)) {
|
||||
try {
|
||||
await deleteGitLabVariable({
|
||||
accessToken,
|
||||
connection,
|
||||
scope,
|
||||
resourceId,
|
||||
key,
|
||||
targetEnvironment,
|
||||
allVariables
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getSecrets: async (secretSync: TGitLabSyncWithCredentials): Promise<TSecretMap> => {
|
||||
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
|
||||
}
|
||||
};
|
@ -0,0 +1,97 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { GitLabSyncScope } from "./gitlab-sync-enums";
|
||||
|
||||
const GitLabSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
|
||||
z.object({
|
||||
scope: z.literal(GitLabSyncScope.Project).describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.scope),
|
||||
projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.projectId),
|
||||
projectName: z
|
||||
.string()
|
||||
.min(1, "Project name is required")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.projectName),
|
||||
targetEnvironment: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("*")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.targetEnvironment),
|
||||
shouldProtectSecrets: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldProtectSecrets),
|
||||
shouldMaskSecrets: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldMaskSecrets),
|
||||
shouldHideSecrets: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldHideSecrets)
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GitLabSyncScope.Group).describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.scope),
|
||||
groupId: z.string().min(1, "Group ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.groupId),
|
||||
groupName: z.string().min(1, "Group name is required").describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.groupName),
|
||||
targetEnvironment: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("*")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.targetEnvironment),
|
||||
shouldProtectSecrets: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldProtectSecrets),
|
||||
shouldMaskSecrets: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldMaskSecrets),
|
||||
shouldHideSecrets: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.GITLAB.shouldHideSecrets)
|
||||
})
|
||||
]);
|
||||
|
||||
const GitLabSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
|
||||
|
||||
export const GitLabSyncSchema = BaseSecretSyncSchema(SecretSync.GitLab, GitLabSyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.GitLab),
|
||||
destinationConfig: GitLabSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateGitLabSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.GitLab,
|
||||
GitLabSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: GitLabSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateGitLabSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.GitLab,
|
||||
GitLabSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: GitLabSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const GitLabSyncListItemSchema = z.object({
|
||||
name: z.literal("GitLab"),
|
||||
connection: z.literal(AppConnection.GitLab),
|
||||
destination: z.literal(SecretSync.GitLab),
|
||||
canImportSecrets: z.literal(false)
|
||||
});
|
58
backend/src/services/secret-sync/gitlab/gitlab-sync-types.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TGitLabConnection } from "@app/services/app-connection/gitlab";
|
||||
|
||||
import { CreateGitLabSyncSchema, GitLabSyncListItemSchema, GitLabSyncSchema } from "./gitlab-sync-schemas";
|
||||
|
||||
export type TGitLabSync = z.infer<typeof GitLabSyncSchema>;
|
||||
export type TGitLabSyncInput = z.infer<typeof CreateGitLabSyncSchema>;
|
||||
export type TGitLabSyncListItem = z.infer<typeof GitLabSyncListItemSchema>;
|
||||
|
||||
export type TGitLabSyncWithCredentials = TGitLabSync & {
|
||||
connection: TGitLabConnection;
|
||||
};
|
||||
|
||||
export type TGitLabVariable = {
|
||||
key: string;
|
||||
value: string;
|
||||
protected: boolean;
|
||||
masked: boolean;
|
||||
environmentScope?: string;
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
export type TGitLabVariableCreate = {
|
||||
key: string;
|
||||
value: string;
|
||||
variable_type?: "env_var" | "file";
|
||||
protected?: boolean;
|
||||
masked?: boolean;
|
||||
raw?: boolean;
|
||||
environment_scope?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type TGitLabVariableUpdate = {
|
||||
value: string;
|
||||
variable_type?: "env_var" | "file";
|
||||
protected?: boolean;
|
||||
masked?: boolean;
|
||||
raw?: boolean;
|
||||
environment_scope?: string;
|
||||
description?: string | null;
|
||||
};
|
||||
|
||||
export type TGitLabListVariables = {
|
||||
accessToken: string;
|
||||
projectId: string;
|
||||
environmentScope?: string;
|
||||
};
|
||||
|
||||
export type TGitLabCreateVariable = TGitLabListVariables & {
|
||||
variable: TGitLabVariableCreate;
|
||||
};
|
||||
|
||||
export type TGitLabUpdateVariable = TGitLabListVariables & {
|
||||
key: string;
|
||||
variable: TGitLabVariableUpdate;
|
||||
};
|
4
backend/src/services/secret-sync/gitlab/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./gitlab-sync-constants";
|
||||
export * from "./gitlab-sync-fns";
|
||||
export * from "./gitlab-sync-schemas";
|
||||
export * from "./gitlab-sync-types";
|
@ -19,6 +19,7 @@ export enum SecretSync {
|
||||
Heroku = "heroku",
|
||||
Render = "render",
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab",
|
||||
CloudflarePages = "cloudflare-pages"
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,7 @@ import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns"
|
||||
import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio";
|
||||
import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
||||
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||
import { GITLAB_SYNC_LIST_OPTION, GitLabSyncFns } from "./gitlab";
|
||||
import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
|
||||
import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku";
|
||||
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||
@ -66,6 +67,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.Heroku]: HEROKU_SYNC_LIST_OPTION,
|
||||
[SecretSync.Render]: RENDER_SYNC_LIST_OPTION,
|
||||
[SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION,
|
||||
[SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION,
|
||||
[SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
@ -230,6 +232,8 @@ export const SecretSyncFns = {
|
||||
return RenderSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Flyio:
|
||||
return FlyioSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.GitLab:
|
||||
return GitLabSyncFns.syncSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
|
||||
case SecretSync.CloudflarePages:
|
||||
return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
@ -318,6 +322,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.Flyio:
|
||||
secretMap = await FlyioSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.GitLab:
|
||||
secretMap = await GitLabSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.CloudflarePages:
|
||||
secretMap = await CloudflarePagesSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
@ -394,6 +401,8 @@ export const SecretSyncFns = {
|
||||
return RenderSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Flyio:
|
||||
return FlyioSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.GitLab:
|
||||
return GitLabSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
|
||||
case SecretSync.CloudflarePages:
|
||||
return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
|
@ -22,6 +22,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.Heroku]: "Heroku",
|
||||
[SecretSync.Render]: "Render",
|
||||
[SecretSync.Flyio]: "Fly.io",
|
||||
[SecretSync.GitLab]: "GitLab",
|
||||
[SecretSync.CloudflarePages]: "Cloudflare Pages"
|
||||
};
|
||||
|
||||
@ -46,6 +47,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.Heroku]: AppConnection.Heroku,
|
||||
[SecretSync.Render]: AppConnection.Render,
|
||||
[SecretSync.Flyio]: AppConnection.Flyio,
|
||||
[SecretSync.GitLab]: AppConnection.GitLab,
|
||||
[SecretSync.CloudflarePages]: AppConnection.Cloudflare
|
||||
};
|
||||
|
||||
@ -70,5 +72,6 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
[SecretSync.Heroku]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Render]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Flyio]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.GitLab]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.CloudflarePages]: SecretSyncPlanType.Regular
|
||||
};
|
||||
|
@ -72,8 +72,15 @@ import {
|
||||
TAzureKeyVaultSyncListItem,
|
||||
TAzureKeyVaultSyncWithCredentials
|
||||
} from "./azure-key-vault";
|
||||
import {
|
||||
TCloudflarePagesSync,
|
||||
TCloudflarePagesSyncInput,
|
||||
TCloudflarePagesSyncListItem,
|
||||
TCloudflarePagesSyncWithCredentials
|
||||
} from "./cloudflare-pages/cloudflare-pages-types";
|
||||
import { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types";
|
||||
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
|
||||
import { TGitLabSync, TGitLabSyncInput, TGitLabSyncListItem, TGitLabSyncWithCredentials } from "./gitlab";
|
||||
import {
|
||||
THCVaultSync,
|
||||
THCVaultSyncInput,
|
||||
@ -106,12 +113,6 @@ import {
|
||||
TTerraformCloudSyncWithCredentials
|
||||
} from "./terraform-cloud";
|
||||
import { TVercelSync, TVercelSyncInput, TVercelSyncListItem, TVercelSyncWithCredentials } from "./vercel";
|
||||
import {
|
||||
TCloudflarePagesSync,
|
||||
TCloudflarePagesSyncInput,
|
||||
TCloudflarePagesSyncListItem,
|
||||
TCloudflarePagesSyncWithCredentials
|
||||
} from "./cloudflare-pages/cloudflare-pages-types";
|
||||
|
||||
export type TSecretSync =
|
||||
| TAwsParameterStoreSync
|
||||
@ -134,6 +135,7 @@ export type TSecretSync =
|
||||
| THerokuSync
|
||||
| TRenderSync
|
||||
| TFlyioSync
|
||||
| TGitLabSync
|
||||
| TCloudflarePagesSync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
@ -157,6 +159,7 @@ export type TSecretSyncWithCredentials =
|
||||
| THerokuSyncWithCredentials
|
||||
| TRenderSyncWithCredentials
|
||||
| TFlyioSyncWithCredentials
|
||||
| TGitLabSyncWithCredentials
|
||||
| TCloudflarePagesSyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput =
|
||||
@ -180,6 +183,7 @@ export type TSecretSyncInput =
|
||||
| THerokuSyncInput
|
||||
| TRenderSyncInput
|
||||
| TFlyioSyncInput
|
||||
| TGitLabSyncInput
|
||||
| TCloudflarePagesSyncInput;
|
||||
|
||||
export type TSecretSyncListItem =
|
||||
@ -203,6 +207,7 @@ export type TSecretSyncListItem =
|
||||
| THerokuSyncListItem
|
||||
| TRenderSyncListItem
|
||||
| TFlyioSyncListItem
|
||||
| TGitLabSyncListItem
|
||||
| TCloudflarePagesSyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
|
35
cli/go.mod
@ -40,6 +40,9 @@ require (
|
||||
golang.org/x/term v0.30.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.31.4
|
||||
k8s.io/apimachinery v0.31.4
|
||||
k8s.io/client-go v0.31.4
|
||||
)
|
||||
|
||||
require (
|
||||
@ -70,16 +73,25 @@ require (
|
||||
github.com/danieljoos/wincred v1.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dvsekhvalnov/jose2go v1.6.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/errors v0.20.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/strfmt v0.21.3 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
@ -90,17 +102,23 @@ require (
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mtibben/percent v0.2.1 // indirect
|
||||
github.com/muesli/mango v0.1.0 // indirect
|
||||
github.com/muesli/mango-pflag v0.1.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
|
||||
github.com/pelletier/go-toml v1.9.3 // indirect
|
||||
@ -117,6 +135,7 @@ require (
|
||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
|
||||
go.mongodb.org/mongo-driver v1.10.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
@ -127,18 +146,26 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/mod v0.23.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.30.0 // indirect
|
||||
google.golang.org/api v0.188.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
|
||||
google.golang.org/grpc v1.64.1 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
|
84
cli/go.sum
@ -134,6 +134,8 @@ github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMS
|
||||
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
||||
github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY=
|
||||
github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@ -152,6 +154,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gitleaks/go-gitdiff v0.9.1 h1:ni6z6/3i9ODT685OLCTf+s/ERlWUNWQF4x1pvoNICw0=
|
||||
github.com/gitleaks/go-gitdiff v0.9.1/go.mod h1:pKz0X4YzCKZs30BL+weqBIG7mx0jl4tF1uXV9ZyNvrA=
|
||||
@ -165,8 +169,16 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8=
|
||||
github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o=
|
||||
github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
@ -174,6 +186,7 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@ -211,6 +224,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
|
||||
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@ -222,9 +237,12 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@ -298,7 +316,11 @@ github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BA
|
||||
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
@ -308,6 +330,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@ -318,6 +341,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
|
||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
@ -346,8 +371,12 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
|
||||
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
|
||||
@ -365,7 +394,8 @@ github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
|
||||
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
@ -406,8 +436,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
|
||||
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
@ -467,6 +497,8 @@ github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8S
|
||||
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
@ -596,8 +628,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -610,8 +642,8 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -693,8 +725,8 @@ golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@ -863,14 +895,17 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@ -890,6 +925,27 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
k8s.io/api v0.31.4 h1:I2QNzitPVsPeLQvexMEsj945QumYraqv9m74isPDKhM=
|
||||
k8s.io/api v0.31.4/go.mod h1:d+7vgXLvmcdT1BCo79VEgJxHHryww3V5np2OYTr6jdw=
|
||||
k8s.io/apimachinery v0.31.4 h1:8xjE2C4CzhYVm9DGf60yohpNUh5AEBnPxCryPBECmlM=
|
||||
k8s.io/apimachinery v0.31.4/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/client-go v0.31.4 h1:t4QEXt4jgHIkKKlx06+W3+1JOwAFU/2OPiOo7H92eRQ=
|
||||
k8s.io/client-go v0.31.4/go.mod h1:kvuMro4sFYIa8sulL5Gi5GFqUPvfH2O/dXuKstbaaeg=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
|
@ -631,8 +631,8 @@ func CallGatewayHeartBeatV1(httpClient *resty.Client) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (map[string]interface{}, error) {
|
||||
var resBody map[string]interface{}
|
||||
func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (BootstrapInstanceResponse, error) {
|
||||
var resBody BootstrapInstanceResponse
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&resBody).
|
||||
@ -641,11 +641,11 @@ func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRe
|
||||
Post(fmt.Sprintf("%v/v1/admin/bootstrap", request.Domain))
|
||||
|
||||
if err != nil {
|
||||
return nil, NewGenericRequestError(operationCallBootstrapInstance, err)
|
||||
return BootstrapInstanceResponse{}, NewGenericRequestError(operationCallBootstrapInstance, err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return nil, NewAPIErrorWithResponse(operationCallBootstrapInstance, response, nil)
|
||||
return BootstrapInstanceResponse{}, NewAPIErrorWithResponse(operationCallBootstrapInstance, response, nil)
|
||||
}
|
||||
|
||||
return resBody, nil
|
||||
|
@ -655,3 +655,35 @@ type BootstrapInstanceRequest struct {
|
||||
Organization string `json:"organization"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type BootstrapInstanceResponse struct {
|
||||
Message string `json:"message"`
|
||||
Identity BootstrapIdentity `json:"identity"`
|
||||
Organization BootstrapOrganization `json:"organization"`
|
||||
User BootstrapUser `json:"user"`
|
||||
}
|
||||
|
||||
type BootstrapIdentity struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Credentials BootstrapIdentityCredentials `json:"credentials"`
|
||||
}
|
||||
|
||||
type BootstrapIdentityCredentials struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type BootstrapOrganization struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
|
||||
type BootstrapUser struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Username string `json:"username"`
|
||||
SuperAdmin bool `json:"superAdmin"`
|
||||
}
|
||||
|
@ -4,16 +4,127 @@ Copyright (c) 2023 Infisical Inc.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// handleK8SecretOutput processes the k8-secret output type by creating a Kubernetes secret
|
||||
func handleK8SecretOutput(bootstrapResponse api.BootstrapInstanceResponse, k8SecretTemplate, k8SecretName, k8SecretNamespace string) error {
|
||||
// Create in-cluster config
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create in-cluster config: %v", err)
|
||||
}
|
||||
|
||||
// Create Kubernetes client
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Kubernetes client: %v", err)
|
||||
}
|
||||
|
||||
// Parse and execute the template to render the data/stringData section
|
||||
tmpl, err := template.New("k8-secret-template").Funcs(template.FuncMap{
|
||||
"encodeBase64": func(s string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(s))
|
||||
},
|
||||
}).Parse(k8SecretTemplate)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse output template: %v", err)
|
||||
}
|
||||
|
||||
var renderedDataSection bytes.Buffer
|
||||
err = tmpl.Execute(&renderedDataSection, bootstrapResponse)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute output template: %v", err)
|
||||
}
|
||||
|
||||
// Parse the rendered template as JSON to validate it's valid
|
||||
var dataSection map[string]interface{}
|
||||
if err := json.Unmarshal(renderedDataSection.Bytes(), &dataSection); err != nil {
|
||||
return fmt.Errorf("template output is not valid JSON: %v", err)
|
||||
}
|
||||
|
||||
// Prepare the secret data and stringData maps
|
||||
secretData := make(map[string][]byte)
|
||||
secretStringData := make(map[string]string)
|
||||
|
||||
// Process the dataSection to separate data and stringData
|
||||
if data, exists := dataSection["data"]; exists {
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
for key, value := range dataMap {
|
||||
if strValue, ok := value.(string); ok {
|
||||
secretData[key] = []byte(strValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if stringData, exists := dataSection["stringData"]; exists {
|
||||
if stringDataMap, ok := stringData.(map[string]interface{}); ok {
|
||||
for key, value := range stringDataMap {
|
||||
if strValue, ok := value.(string); ok {
|
||||
secretStringData[key] = strValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the Kubernetes secret object
|
||||
k8sSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: k8SecretName,
|
||||
Namespace: k8SecretNamespace,
|
||||
},
|
||||
Type: corev1.SecretTypeOpaque,
|
||||
Data: secretData,
|
||||
StringData: secretStringData,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
secretsClient := clientset.CoreV1().Secrets(k8SecretNamespace)
|
||||
|
||||
// Check if secret already exists
|
||||
existingSecret, err := secretsClient.Get(ctx, k8SecretName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
// Secret doesn't exist, create it
|
||||
_, err = secretsClient.Create(ctx, k8sSecret, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Kubernetes secret: %v", err)
|
||||
}
|
||||
log.Info().Msgf("Successfully created Kubernetes secret '%s' in namespace '%s'", k8SecretName, k8SecretNamespace)
|
||||
} else {
|
||||
return fmt.Errorf("failed to check if Kubernetes secret exists: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Secret exists, update it
|
||||
k8sSecret.ObjectMeta.ResourceVersion = existingSecret.ObjectMeta.ResourceVersion
|
||||
_, err = secretsClient.Update(ctx, k8sSecret, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update Kubernetes secret: %v", err)
|
||||
}
|
||||
log.Info().Msgf("Successfully updated Kubernetes secret '%s' in namespace '%s'", k8SecretName, k8SecretNamespace)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var bootstrapCmd = &cobra.Command{
|
||||
Use: "bootstrap",
|
||||
Short: "Used to bootstrap your Infisical instance",
|
||||
@ -23,7 +134,7 @@ var bootstrapCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
email, _ := cmd.Flags().GetString("email")
|
||||
if email == "" {
|
||||
if envEmail, ok := os.LookupEnv("INFISICAL_ADMIN_EMAIL"); ok {
|
||||
if envEmail, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_EMAIL_NAME); ok {
|
||||
email = envEmail
|
||||
}
|
||||
}
|
||||
@ -35,7 +146,7 @@ var bootstrapCmd = &cobra.Command{
|
||||
|
||||
password, _ := cmd.Flags().GetString("password")
|
||||
if password == "" {
|
||||
if envPassword, ok := os.LookupEnv("INFISICAL_ADMIN_PASSWORD"); ok {
|
||||
if envPassword, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_PASSWORD_NAME); ok {
|
||||
password = envPassword
|
||||
}
|
||||
}
|
||||
@ -47,7 +158,7 @@ var bootstrapCmd = &cobra.Command{
|
||||
|
||||
organization, _ := cmd.Flags().GetString("organization")
|
||||
if organization == "" {
|
||||
if envOrganization, ok := os.LookupEnv("INFISICAL_ADMIN_ORGANIZATION"); ok {
|
||||
if envOrganization, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_ORGANIZATION_NAME); ok {
|
||||
organization = envOrganization
|
||||
}
|
||||
}
|
||||
@ -69,11 +180,56 @@ var bootstrapCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
outputType, err := cmd.Flags().GetString("output")
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to get output type: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
k8SecretTemplate, err := cmd.Flags().GetString("k8-secret-template")
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to get k8-secret-template: %v", err)
|
||||
}
|
||||
|
||||
k8SecretName, err := cmd.Flags().GetString("k8-secret-name")
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to get k8-secret-name: %v", err)
|
||||
}
|
||||
|
||||
k8SecretNamespace, err := cmd.Flags().GetString("k8-secret-namespace")
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to get k8-secret-namespace: %v", err)
|
||||
}
|
||||
|
||||
if outputType == "k8-secret" {
|
||||
if k8SecretTemplate == "" {
|
||||
log.Error().Msg("k8-secret-template is required when using k8-secret output type")
|
||||
return
|
||||
}
|
||||
|
||||
if k8SecretName == "" {
|
||||
log.Error().Msg("k8-secret-name is required when using k8-secret output type")
|
||||
return
|
||||
}
|
||||
|
||||
if k8SecretNamespace == "" {
|
||||
log.Error().Msg("k8-secret-namespace is required when using k8-secret output type")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to get resty client with custom headers: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ignoreIfBootstrapped, err := cmd.Flags().GetBool("ignore-if-bootstrapped")
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to get ignore-if-bootstrapped flag: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
httpClient.SetHeader("Accept", "application/json")
|
||||
|
||||
bootstrapResponse, err := api.CallBootstrapInstance(httpClient, api.BootstrapInstanceRequest{
|
||||
@ -84,16 +240,26 @@ var bootstrapCmd = &cobra.Command{
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to bootstrap instance: %v", err)
|
||||
if !ignoreIfBootstrapped {
|
||||
log.Error().Msgf("Failed to bootstrap instance: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal().Msgf("Failed to convert response to JSON: %v", err)
|
||||
return
|
||||
if outputType == "k8-secret" {
|
||||
if err := handleK8SecretOutput(bootstrapResponse, k8SecretTemplate, k8SecretName, k8SecretNamespace); err != nil {
|
||||
log.Error().Msgf("Failed to handle k8-secret output: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal().Msgf("Failed to convert response to JSON: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(string(responseJSON))
|
||||
}
|
||||
fmt.Println(string(responseJSON))
|
||||
},
|
||||
}
|
||||
|
||||
@ -102,6 +268,10 @@ func init() {
|
||||
bootstrapCmd.Flags().String("email", "", "The desired email address of the instance admin")
|
||||
bootstrapCmd.Flags().String("password", "", "The desired password of the instance admin")
|
||||
bootstrapCmd.Flags().String("organization", "", "The name of the organization to create for the instance")
|
||||
|
||||
bootstrapCmd.Flags().String("output", "", "The type of output to use for the bootstrap command (json or k8-secret)")
|
||||
bootstrapCmd.Flags().Bool("ignore-if-bootstrapped", false, "Whether to continue on error if the instance has already been bootstrapped")
|
||||
bootstrapCmd.Flags().String("k8-secret-template", "{\"data\":{\"token\":\"{{.Identity.Credentials.Token}}\"}}", "The template to use for rendering the Kubernetes secret (entire secret JSON)")
|
||||
bootstrapCmd.Flags().String("k8-secret-namespace", "", "The namespace to create the Kubernetes secret in")
|
||||
bootstrapCmd.Flags().String("k8-secret-name", "", "The name of the Kubernetes secret to create")
|
||||
rootCmd.AddCommand(bootstrapCmd)
|
||||
}
|
||||
|
@ -10,6 +10,10 @@ const (
|
||||
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
|
||||
INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase.
|
||||
|
||||
INFISICAL_BOOTSTRAP_EMAIL_NAME = "INFISICAL_ADMIN_EMAIL"
|
||||
INFISICAL_BOOTSTRAP_PASSWORD_NAME = "INFISICAL_ADMIN_PASSWORD"
|
||||
INFISICAL_BOOTSTRAP_ORGANIZATION_NAME = "INFISICAL_ADMIN_ORGANIZATION"
|
||||
|
||||
VAULT_BACKEND_AUTO_MODE = "auto"
|
||||
VAULT_BACKEND_FILE_MODE = "file"
|
||||
|
||||
@ -47,6 +51,11 @@ const (
|
||||
|
||||
INFISICAL_BACKUP_SECRET = "infisical-backup-secrets" // akhilmhdh: @depreciated remove in version v0.30
|
||||
INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY = "infisical-backup-secret-encryption-key"
|
||||
|
||||
KUBERNETES_SERVICE_HOST_ENV_NAME = "KUBERNETES_SERVICE_HOST"
|
||||
KUBERNETES_SERVICE_PORT_HTTPS_ENV_NAME = "KUBERNETES_SERVICE_PORT_HTTPS"
|
||||
KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||
KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/gitlab/available"
|
||||
---
|
@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/gitlab"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Gitlab OAuth Connections must be created through the Infisical UI.
|
||||
Check out the configuration docs for [Gitlab OAuth Connections](/integrations/app-connections/gitlab) for a step-by-step
|
||||
guide.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/gitlab/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/gitlab/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/gitlab/connection-name/{connectionName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/gitlab"
|
||||
---
|
@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/app-connections/gitlab/{connectionId}"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Gitlab OAuth Connections must be updated through the Infisical UI.
|
||||
Check out the configuration docs for [Gitlab OAuth Connections](/integrations/app-connections/gitlab) for a step-by-step
|
||||
guide.
|
||||
</Note>
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/secret-syncs/gitlab"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/secret-syncs/gitlab/{syncId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/secret-syncs/gitlab/{syncId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/secret-syncs/gitlab/sync-name/{syncName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/secret-syncs/gitlab"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Remove Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/gitlab/{syncId}/remove-secrets"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Sync Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/gitlab/{syncId}/sync-secrets"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/secret-syncs/gitlab/{syncId}"
|
||||
---
|
@ -75,8 +75,90 @@ This flag is required.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--ignore-if-bootstrapped">
|
||||
Whether to continue without error if the instance has already been bootstrapped. Useful for idempotent automation scripts.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical bootstrap --ignore-if-bootstrapped
|
||||
```
|
||||
|
||||
This flag is optional and defaults to `false`.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--output">
|
||||
The type of output format for the bootstrap command. Supports `k8-secret` for Kubernetes secret integration. This flag is optional and defaults to "".
|
||||
|
||||
```bash
|
||||
# Kubernetes secret output
|
||||
infisical bootstrap --output=k8-secret --k8-secret-template='{"data":{"token":"{{.Identity.Credentials.Token}}"}}' --k8-secret-name=infisical-bootstrap --k8-secret-namespace=default
|
||||
```
|
||||
|
||||
When using `k8-secret`, the command will create or update a Kubernetes secret directly in your cluster. Note that this option requires the command to be executed from within a Kubernetes pod with appropriate service account permissions.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--k8-secret-template">
|
||||
The template to use for rendering the Kubernetes secret data/stringData section. Required when using `--output=k8-secret`. The template uses Go template syntax and has access to the bootstrap response data.
|
||||
|
||||
```bash
|
||||
# Example template that stores the token
|
||||
infisical bootstrap --k8-secret-template='{"data":{"token":"{{.Identity.Credentials.Token}}"}}'
|
||||
|
||||
# Example template with multiple fields
|
||||
infisical bootstrap --k8-secret-template='{"stringData":{"token":"{{.Identity.Credentials.Token}}","org-id":"{{.Organization.ID}}","user-email":"{{.User.Email}}"}}'
|
||||
```
|
||||
|
||||
Available template functions:
|
||||
|
||||
- `encodeBase64`: Base64 encode a string
|
||||
|
||||
Available data fields:
|
||||
|
||||
- `.Identity.Credentials.Token`: The machine identity token
|
||||
- `.Identity.ID`: The identity ID
|
||||
- `.Identity.Name`: The identity name
|
||||
- `.Organization.ID`: The organization ID
|
||||
- `.Organization.Name`: The organization name
|
||||
- `.Organization.Slug`: The organization slug
|
||||
- `.User.Email`: The admin user email
|
||||
- `.User.ID`: The admin user ID
|
||||
- `.User.FirstName`: The admin user first name
|
||||
- `.User.LastName`: The admin user last name
|
||||
|
||||
This flag is required when using `k8-secret` output.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--k8-secret-name">
|
||||
The name of the Kubernetes secret to create or update. Required when using `--output=k8-secret`.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical bootstrap --k8-secret-name=infisical-bootstrap-credentials
|
||||
```
|
||||
|
||||
This flag is required when using `k8-secret` output.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--k8-secret-namespace">
|
||||
The namespace where the Kubernetes secret should be created or updated. Required when using `--output=k8-secret`.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical bootstrap --k8-secret-namespace=infisical-system
|
||||
```
|
||||
|
||||
This flag is required when using `k8-secret` output.
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Response
|
||||
|
||||
### JSON Output (Default)
|
||||
|
||||
The command returns a JSON response with details about the created user, organization, and machine identity:
|
||||
|
||||
```json
|
||||
@ -105,6 +187,47 @@ The command returns a JSON response with details about the created user, organiz
|
||||
}
|
||||
```
|
||||
|
||||
### Kubernetes Secret Output
|
||||
|
||||
When using `--output=k8-secret`, the command creates or updates a Kubernetes secret in your cluster and logs the operation result. This is particularly useful for automated bootstrapping scenarios such as Kubernetes Jobs, GitOps workflows, or when you need to immediately store the admin credentials for use by other applications in your cluster.
|
||||
|
||||
## Kubernetes Integration
|
||||
|
||||
### Prerequisites for k8-secret Output
|
||||
|
||||
When running with `--output=k8-secret`, the command must be executed from within a Kubernetes pod with proper service account permissions. The command automatically:
|
||||
|
||||
1. Reads the service account token from `/var/run/secrets/kubernetes.io/serviceaccount/token`
|
||||
2. Reads the CA certificate from `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`
|
||||
3. Gets the Kubernetes API server URL from environment variables (`KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT_HTTPS`)
|
||||
|
||||
### Required RBAC Permissions
|
||||
|
||||
Your service account needs the following permissions:
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: infisical-bootstrap
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "create", "update"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: infisical-bootstrap
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: your-service-account
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: infisical-bootstrap
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
## Usage with Automation
|
||||
|
||||
For automation purposes, you can extract just the machine identity token from the response:
|
||||
@ -127,6 +250,8 @@ echo "Token has been captured and can be used for authentication"
|
||||
## Notes
|
||||
|
||||
- The bootstrap process can only be performed once on a fresh Infisical instance
|
||||
- All flags are required for the bootstrap process to complete successfully
|
||||
- All core flags (domain, email, password, organization) are required for the bootstrap process to complete successfully
|
||||
- Security controls prevent privilege escalation: instance admin identities cannot be managed by non-instance admin users and identities
|
||||
- The generated admin user account can be used to log in via the UI if needed
|
||||
- When using `k8-secret` output, the command must run within a Kubernetes pod with proper service account permissions
|
||||
- The `--ignore-if-bootstrapped` flag is useful for making bootstrap scripts idempotent
|
||||
|
@ -475,6 +475,7 @@
|
||||
"integrations/app-connections/gcp",
|
||||
"integrations/app-connections/github",
|
||||
"integrations/app-connections/github-radar",
|
||||
"integrations/app-connections/gitlab",
|
||||
"integrations/app-connections/hashicorp-vault",
|
||||
"integrations/app-connections/heroku",
|
||||
"integrations/app-connections/humanitec",
|
||||
@ -512,6 +513,7 @@
|
||||
"integrations/secret-syncs/flyio",
|
||||
"integrations/secret-syncs/gcp-secret-manager",
|
||||
"integrations/secret-syncs/github",
|
||||
"integrations/secret-syncs/gitlab",
|
||||
"integrations/secret-syncs/hashicorp-vault",
|
||||
"integrations/secret-syncs/heroku",
|
||||
"integrations/secret-syncs/humanitec",
|
||||
@ -1317,6 +1319,18 @@
|
||||
"api-reference/endpoints/app-connections/github/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "GitLab",
|
||||
"pages": [
|
||||
"api-reference/endpoints/app-connections/gitlab/list",
|
||||
"api-reference/endpoints/app-connections/gitlab/available",
|
||||
"api-reference/endpoints/app-connections/gitlab/get-by-id",
|
||||
"api-reference/endpoints/app-connections/gitlab/get-by-name",
|
||||
"api-reference/endpoints/app-connections/gitlab/create",
|
||||
"api-reference/endpoints/app-connections/gitlab/update",
|
||||
"api-reference/endpoints/app-connections/gitlab/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "GitHub Radar",
|
||||
"pages": [
|
||||
@ -1667,6 +1681,19 @@
|
||||
"api-reference/endpoints/secret-syncs/github/remove-secrets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "GitLab",
|
||||
"pages": [
|
||||
"api-reference/endpoints/secret-syncs/gitlab/list",
|
||||
"api-reference/endpoints/secret-syncs/gitlab/get-by-id",
|
||||
"api-reference/endpoints/secret-syncs/gitlab/get-by-name",
|
||||
"api-reference/endpoints/secret-syncs/gitlab/create",
|
||||
"api-reference/endpoints/secret-syncs/gitlab/update",
|
||||
"api-reference/endpoints/secret-syncs/gitlab/delete",
|
||||
"api-reference/endpoints/secret-syncs/gitlab/sync-secrets",
|
||||
"api-reference/endpoints/secret-syncs/gitlab/remove-secrets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Hashicorp Vault",
|
||||
"pages": [
|
||||
|
Before Width: | Height: | Size: 759 KiB After Width: | Height: | Size: 208 KiB |
After Width: | Height: | Size: 593 KiB |
Before Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 935 KiB |
Before Width: | Height: | Size: 497 KiB After Width: | Height: | Size: 344 KiB |
BIN
docs/images/app-connections/gitlab/gitlab-applications.png
Normal file
After Width: | Height: | Size: 294 KiB |
BIN
docs/images/app-connections/gitlab/gitlab-authorization-page.png
Normal file
After Width: | Height: | Size: 196 KiB |
BIN
docs/images/app-connections/gitlab/gitlab-config-credentials.png
Normal file
After Width: | Height: | Size: 260 KiB |
Before Width: | Height: | Size: 540 KiB After Width: | Height: | Size: 380 KiB |
After Width: | Height: | Size: 531 KiB |
After Width: | Height: | Size: 480 KiB |
BIN
docs/images/app-connections/gitlab/gitlab-dashboard.png
Normal file
After Width: | Height: | Size: 284 KiB |
BIN
docs/images/app-connections/gitlab/gitlab-oauth-connection.png
Normal file
After Width: | Height: | Size: 917 KiB |
After Width: | Height: | Size: 426 KiB |
After Width: | Height: | Size: 708 KiB |
After Width: | Height: | Size: 464 KiB |
After Width: | Height: | Size: 782 KiB |
Before Width: | Height: | Size: 592 KiB |
BIN
docs/images/secret-syncs/gitlab/gitlab-secret-sync-created.png
Normal file
After Width: | Height: | Size: 946 KiB |
After Width: | Height: | Size: 202 KiB |
BIN
docs/images/secret-syncs/gitlab/gitlab-secret-sync-details.png
Normal file
After Width: | Height: | Size: 582 KiB |
BIN
docs/images/secret-syncs/gitlab/gitlab-secret-sync-option.png
Normal file
After Width: | Height: | Size: 646 KiB |
BIN
docs/images/secret-syncs/gitlab/gitlab-secret-sync-options.png
Normal file
After Width: | Height: | Size: 636 KiB |
BIN
docs/images/secret-syncs/gitlab/gitlab-secret-sync-review.png
Normal file
After Width: | Height: | Size: 618 KiB |
BIN
docs/images/secret-syncs/gitlab/gitlab-secret-sync-source.png
Normal file
After Width: | Height: | Size: 569 KiB |
192
docs/integrations/app-connections/gitlab.mdx
Normal file
@ -0,0 +1,192 @@
|
||||
---
|
||||
title: "GitLab Connection"
|
||||
description: "Learn how to configure a GitLab Connection for Infisical using OAuth or Access Token methods."
|
||||
---
|
||||
|
||||
Infisical supports two methods for connecting to GitLab: **OAuth** and **Access Token**. Choose the method that best fits your setup and security requirements.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="OAuth Method">
|
||||
The OAuth method provides secure authentication through GitLab's OAuth flow.
|
||||
|
||||
<Accordion title="Self-Hosted Instance Setup">
|
||||
Using the GitLab Connection with OAuth on a self-hosted instance of Infisical requires configuring an OAuth application in GitLab and registering your instance with it.
|
||||
|
||||
**Prerequisites:**
|
||||
- A GitLab account with existing projects
|
||||
- Self-hosted Infisical instance
|
||||
|
||||
<Steps>
|
||||
<Step title="Create an OAuth application in GitLab">
|
||||
Navigate to your user Settings > Applications to create a new GitLab application.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/organization/app-connections/gitlab/oauth/callback`.
|
||||
|
||||

|
||||

|
||||
|
||||
<Tip>
|
||||
The domain you defined in the Redirect URI should be equivalent to the `SITE_URL` configured in your Infisical instance.
|
||||
</Tip>
|
||||
|
||||
<Note>
|
||||
If you have a GitLab group, you can create an OAuth application under it in your group Settings > Applications.
|
||||
</Note>
|
||||
</Step>
|
||||
<Step title="Add your GitLab OAuth application credentials to Infisical">
|
||||
Obtain the **Application ID** and **Secret** for your GitLab OAuth application.
|
||||
|
||||

|
||||
|
||||
Back in your Infisical instance, add two new environment variables for the credentials of your GitLab OAuth application:
|
||||
|
||||
- `INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID`: The **Application ID** of your GitLab OAuth application.
|
||||
- `INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET`: The **Secret** of your GitLab OAuth application.
|
||||
|
||||
Once added, restart your Infisical instance and use the GitLab Connection.
|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
## Setup GitLab OAuth Connection in Infisical
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to App Connections">
|
||||
Navigate to the **App Connections** tab on the **Organization Settings** page.
|
||||

|
||||
</Step>
|
||||
<Step title="Add Connection">
|
||||
Select the **GitLab Connection** option from the connection options modal.
|
||||

|
||||
</Step>
|
||||
<Step title="Choose OAuth Method">
|
||||
Select the **OAuth** method and click **Connect to GitLab**.
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Grant Access">
|
||||
You will be redirected to GitLab to grant Infisical access to your GitLab account. Once granted, you will be redirected back to Infisical's App Connections page.
|
||||

|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
Your **GitLab Connection** is now available for use.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Access Token Method">
|
||||
The Access Token method uses a GitLab access token for authentication, providing a straightforward setup process.
|
||||
|
||||
## Generate GitLab Access Token
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Personal Access Token">
|
||||
Personal access tokens provide access to your GitLab account and all projects you have access to.
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to Access Tokens">
|
||||
Log in to your GitLab account and navigate to User Settings > Access tokens. Click **Add new token** to create a new personal access token.
|
||||
|
||||

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

|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Info>
|
||||
Personal Access Token connections require manual token rotation when your GitLab access token expires or is regenerated. Monitor your connection status and update the token as needed.
|
||||
</Info>
|
||||
</Step>
|
||||
<Step title="Copy Token">
|
||||
Copy the generated token immediately as it won't be shown again.
|
||||
|
||||

|
||||
|
||||
<Warning>
|
||||
Keep your access token secure and do not share it. Anyone with access to this token can access your GitLab account and projects.
|
||||
</Warning>
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
|
||||
<Tab title="Project Access Token">
|
||||
Project access tokens provide access to a specific GitLab project, offering more granular control.
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to Project Settings">
|
||||
Go to your GitLab project and navigate to Settings > Access Tokens. Click **Add new token** to create a new project access token.
|
||||
|
||||

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

|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Info>
|
||||
Project Access Token connections require manual token rotation when your GitLab access token expires or is regenerated. Monitor your connection status and update the token as needed.
|
||||
</Info>
|
||||
</Step>
|
||||
<Step title="Copy Token">
|
||||
Copy the generated token immediately as it won't be shown again.
|
||||
|
||||

|
||||
|
||||
<Warning>
|
||||
Keep your access token secure and do not share it. Anyone with access to this token can access your GitLab account and projects.
|
||||
</Warning>
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Setup GitLab Access Token Connection in Infisical
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to App Connections">
|
||||
Navigate to the **App Connections** tab on the **Organization Settings** page.
|
||||

|
||||
</Step>
|
||||
<Step title="Add Connection">
|
||||
Select the **GitLab Connection** option from the connection options modal.
|
||||

|
||||
</Step>
|
||||
<Step title="Configure Access Token">
|
||||
Select the **Access Token** method, paste your GitLab access token in the provided field, and select the appropriate token type.
|
||||
|
||||

|
||||
|
||||
Click **Connect** to establish the connection.
|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
Your **GitLab Connection** is now available for use.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Heroku App Connection"
|
||||
description: "Learn how to configure a Heroku App Connection for Infisical using OAuth or Auth Token methods."
|
||||
title: "Heroku Connection"
|
||||
description: "Learn how to configure a Heroku Connection for Infisical using OAuth or Auth Token methods."
|
||||
---
|
||||
|
||||
Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth Token**. Choose the method that best fits your setup and security requirements.
|
||||
@ -10,7 +10,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To
|
||||
The OAuth method provides secure authentication through Heroku's OAuth flow.
|
||||
|
||||
<Accordion title="Self-Hosted Instance Setup">
|
||||
Using the Heroku App Connection with OAuth on a self-hosted instance of Infisical requires configuring an API client in Heroku and registering your instance with it.
|
||||
Using the Heroku Connection with OAuth on a self-hosted instance of Infisical requires configuring an API client in Heroku and registering your instance with it.
|
||||
|
||||
**Prerequisites:**
|
||||
- A Heroku account with existing applications
|
||||
@ -42,7 +42,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To
|
||||
- `CLIENT_ID_HEROKU`: The **Client ID** of your Heroku API client.
|
||||
- `CLIENT_SECRET_HEROKU`: The **Client Secret** of your Heroku API client.
|
||||
|
||||
Once added, restart your Infisical instance and use the Heroku App Connection.
|
||||
Once added, restart your Infisical instance and use the Heroku Connection.
|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
@ -55,7 +55,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To
|
||||

|
||||
</Step>
|
||||
<Step title="Add Connection">
|
||||
Select the **Heroku App Connection** option from the connection options modal.
|
||||
Select the **Heroku Connection** option from the connection options modal.
|
||||

|
||||
</Step>
|
||||
<Step title="Choose OAuth Method">
|
||||
@ -68,7 +68,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To
|
||||

|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
Your **Heroku App Connection** is now available for use.
|
||||
Your **Heroku Connection** is now available for use.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
@ -97,7 +97,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To
|
||||

|
||||
</Step>
|
||||
<Step title="Add Connection">
|
||||
Select the **Heroku App Connection** option from the connection options modal.
|
||||
Select the **Heroku Connection** option from the connection options modal.
|
||||

|
||||
</Step>
|
||||
<Step title="Configure Auth Token">
|
||||
@ -108,7 +108,7 @@ Infisical supports two methods for connecting to Heroku: **OAuth** and **Auth To
|
||||
Click **Connect** to establish the connection.
|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
Your **Heroku App Connection** is now available for use.
|
||||
Your **Heroku Connection** is now available for use.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
|
@ -148,3 +148,11 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## FAQ
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="What's the relationship between 'path' and 'key schema'?">
|
||||
The path is required and will be prepended to the key schema. For example, if you have a path of `/demo/path/` and a key schema of `INFISICAL_{{secretKey}}`, then the result will be `/demo/path/INFISICAL_{{secretKey}}`.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
180
docs/integrations/secret-syncs/gitlab.mdx
Normal file
@ -0,0 +1,180 @@
|
||||
---
|
||||
title: "GitLab Sync"
|
||||
description: "Learn how to configure a GitLab Sync for Infisical."
|
||||
---
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||
- Create a [GitLab Connection](/integrations/app-connections/gitlab)
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button.
|
||||

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

|
||||
|
||||
3. Configure the **Source** from where secrets should be retrieved, then click **Next**.
|
||||

|
||||
|
||||
- **Environment**: The project environment to retrieve secrets from.
|
||||
- **Secret Path**: The folder path to retrieve secrets from.
|
||||
|
||||
<Tip>
|
||||
If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
|
||||
</Tip>
|
||||
|
||||
4. Configure the **Destination** to where secrets should be deployed, then click **Next**.
|
||||

|
||||
|
||||
- **GitLab Connection**: The GitLab Connection to authenticate with.
|
||||
- **Scope**: The GitLab scope to sync secrets to.
|
||||
- **Project**: Sync secrets to a GitLab project.
|
||||
- **Group**: Sync secrets to a GitLab group.
|
||||
<p class="height:1px" />
|
||||
The remaining fields are determined by the selected **Scope**:
|
||||
<AccordionGroup>
|
||||
<Accordion title="Project">
|
||||
- **GitLab Project**: The project to deploy secrets to.
|
||||
- **GitLab Environment Scope**: The environment scope to deploy secrets to (optional, defaults to "*" for all environments).
|
||||
- **Mark secrets as Protected**: If enabled, synced secrets will be marked as protected in GitLab.
|
||||
- **Mark secrets as Masked**: If enabled, synced secrets will be masked in GitLab CI/CD logs.
|
||||
- **Mark secrets as Hidden**: If enabled, synced secrets will be hidden from the GitLab UI.
|
||||
</Accordion>
|
||||
<Accordion title="Group">
|
||||
- **GitLab Group**: The group to deploy secrets to.
|
||||
- **GitLab Environment Scope**: The environment scope to deploy secrets to (optional, defaults to "*" for all environments).
|
||||
- **Mark secrets as Protected**: If enabled, synced secrets will be marked as protected in GitLab.
|
||||
- **Mark secrets as Masked**: If enabled, synced secrets will be masked in GitLab CI/CD logs.
|
||||
- **Mark secrets as Hidden**: If enabled, synced secrets will be hidden from the GitLab UI.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
<Note>
|
||||
Be aware that GitLab only allows to mark secrets as hidden for new secrets. If you try to mark an existing secret as hidden, it produces an error.
|
||||
</Note>
|
||||
<Warning>
|
||||
If you enable **Mark secrets as Hidden**, Infisical will not be able to unhide/unmask secrets from the sync destination if you disable the option later. This is because GitLab does not allow to unhide/unmask existing secrets.
|
||||
</Warning>
|
||||
|
||||
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
||||

|
||||
|
||||
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
|
||||
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
||||
<Note>
|
||||
GitLab does not support importing secrets.
|
||||
</Note>
|
||||
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment.
|
||||
<Note>
|
||||
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
|
||||
</Note>
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
|
||||
|
||||
6. Configure the **Details** of your GitLab Sync, then click **Next**.
|
||||

|
||||
|
||||
- **Name**: The name of your sync. Must be slug-friendly.
|
||||
- **Description**: An optional description for your sync.
|
||||
|
||||
7. Review your GitLab Sync configuration, then click **Create Sync**.
|
||||

|
||||
|
||||
8. If enabled, your GitLab Sync will begin syncing your secrets to the destination endpoint.
|
||||

|
||||
|
||||
</Tab>
|
||||
<Tab title="API">
|
||||
To create a **GitLab Sync**, make an API request to the [Create GitLab Sync](/api-reference/endpoints/secret-syncs/gitlab/create) API endpoint.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://app.infisical.com/api/v1/secret-syncs/gitlab \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "my-gitlab-sync",
|
||||
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"description": "an example sync",
|
||||
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"environment": "dev",
|
||||
"secretPath": "/my-secrets",
|
||||
"isEnabled": true,
|
||||
"syncOptions": {
|
||||
"initialSyncBehavior": "overwrite-destination"
|
||||
},
|
||||
"destinationConfig": {
|
||||
"scope": "project",
|
||||
"projectId": "70998370",
|
||||
"projectName": "test",
|
||||
"targetEnvironment": "*",
|
||||
"shouldProtectSecrets": true,
|
||||
"shouldMaskSecrets": true,
|
||||
"shouldHideSecrets": false
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Sample response
|
||||
|
||||
```bash Response
|
||||
{
|
||||
"secretSync": {
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"name": "my-gitlab-sync",
|
||||
"description": "an example sync",
|
||||
"isEnabled": true,
|
||||
"version": 1,
|
||||
"folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"createdAt": "2023-11-07T05:31:56Z",
|
||||
"updatedAt": "2023-11-07T05:31:56Z",
|
||||
"syncStatus": "succeeded",
|
||||
"lastSyncJobId": "123",
|
||||
"lastSyncMessage": null,
|
||||
"lastSyncedAt": "2023-11-07T05:31:56Z",
|
||||
"importStatus": null,
|
||||
"lastImportJobId": null,
|
||||
"lastImportMessage": null,
|
||||
"lastImportedAt": null,
|
||||
"removeStatus": null,
|
||||
"lastRemoveJobId": null,
|
||||
"lastRemoveMessage": null,
|
||||
"lastRemovedAt": null,
|
||||
"syncOptions": {
|
||||
"initialSyncBehavior": "overwrite-destination"
|
||||
},
|
||||
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"connection": {
|
||||
"app": "gitlab",
|
||||
"name": "my-gitlab-connection",
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
|
||||
},
|
||||
"environment": {
|
||||
"slug": "dev",
|
||||
"name": "Development",
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
|
||||
},
|
||||
"folder": {
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"path": "/my-secrets"
|
||||
},
|
||||
"destination": "gitlab",
|
||||
"destinationConfig": {
|
||||
"scope": "project",
|
||||
"projectId": "70998370",
|
||||
"projectName": "test",
|
||||
"targetEnvironment": "*",
|
||||
"shouldProtectSecrets": true,
|
||||
"shouldMaskSecrets": true,
|
||||
"shouldHideSecrets": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
@ -6,7 +6,7 @@ description: "Learn how to configure a Heroku Sync for Infisical."
|
||||
**Prerequisites:**
|
||||
|
||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||
- Create a [Heroku App Connection](/integrations/app-connections/heroku)
|
||||
- Create a [Heroku Connection](/integrations/app-connections/heroku)
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
@ -29,7 +29,7 @@ description: "Learn how to configure a Heroku Sync for Infisical."
|
||||
4. Configure the **Destination** to where secrets should be deployed, then click **Next**.
|
||||

|
||||
|
||||
- **Heroku App Connection**: The Heroku App Connection to authenticate with.
|
||||
- **Heroku Connection**: The Heroku Connection to authenticate with.
|
||||
- **Heroku App**: The Heroku application to sync secrets to.
|
||||
|
||||
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
||||
|
2241
docs/mint.json
Normal file
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Infisical Java SDK"
|
||||
sidebarTitle: "Java"
|
||||
url: "https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-nodejs-sdk"
|
||||
url: "https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-java-sdk"
|
||||
icon: "java"
|
||||
---
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Infisical Node.js SDK"
|
||||
sidebarTitle: "Node.js"
|
||||
url: "https://github.com/Infisical/node-sdk-v2"
|
||||
url: "https://github.com/Infisical/node-sdk-v2?tab=readme-ov-file#infisical-nodejs-sdk"
|
||||
icon: "node"
|
||||
---
|
||||
|
||||
|
@ -43,7 +43,7 @@ def hello_world():
|
||||
This example demonstrates how to use the Infisical Python SDK with a Flask application. The application retrieves a secret named "NAME" and responds to requests with a greeting that includes the secret value.
|
||||
|
||||
<Warning>
|
||||
We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best.
|
||||
We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best.
|
||||
</Warning>
|
||||
|
||||
## Installation
|
||||
@ -314,32 +314,32 @@ By default, `getSecret()` fetches and returns a shared secret. If not found, it
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to retrieve
|
||||
</ParamField>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to retrieve
|
||||
</ParamField>
|
||||
<ParamField query="include_imports" type="boolean">
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "personal".
|
||||
</ParamField>
|
||||
<ParamField query="include_imports" type="boolean" default="false" optional>
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "personal".
|
||||
</ParamField>
|
||||
<ParamField query="include_imports" type="boolean" default="false" optional>
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
<ParamField query="expand_secret_references" type="boolean" default="true" optional>
|
||||
Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### client.createSecret(options)
|
||||
@ -358,26 +358,26 @@ Create a new secret in Infisical.
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to create.
|
||||
</ParamField>
|
||||
<ParamField query="secret_value" type="string" required>
|
||||
The value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be created.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to create.
|
||||
</ParamField>
|
||||
<ParamField query="secret_value" type="string" required>
|
||||
The value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be created.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### client.updateSecret(options)
|
||||
@ -396,26 +396,26 @@ Update an existing secret in Infisical.
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="secret_value" type="string" required>
|
||||
The new value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be updated.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="secret_value" type="string" required>
|
||||
The new value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be updated.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### client.deleteSecret(options)
|
||||
@ -433,23 +433,23 @@ Delete a secret in Infisical.
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string">
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be deleted.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string">
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be deleted.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
## Cryptography
|
||||
@ -480,14 +480,14 @@ encryptedData = client.encryptSymmetric(encryptOptions)
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" required>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="plaintext" type="string">
|
||||
The plaintext you want to encrypt.
|
||||
</ParamField>
|
||||
<ParamField query="key" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="plaintext" type="string">
|
||||
The plaintext you want to encrypt.
|
||||
</ParamField>
|
||||
<ParamField query="key" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
#### Returns (object)
|
||||
@ -512,20 +512,20 @@ decryptedString = client.decryptSymmetric(decryptOptions)
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" required>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ciphertext" type="string">
|
||||
The ciphertext you want to decrypt.
|
||||
</ParamField>
|
||||
<ParamField query="key" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
<ParamField query="iv" type="string" required>
|
||||
The initialization vector to use for decryption.
|
||||
</ParamField>
|
||||
<ParamField query="tag" type="string" required>
|
||||
The authentication tag to use for decryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ciphertext" type="string">
|
||||
The ciphertext you want to decrypt.
|
||||
</ParamField>
|
||||
<ParamField query="key" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
<ParamField query="iv" type="string" required>
|
||||
The initialization vector to use for decryption.
|
||||
</ParamField>
|
||||
<ParamField query="tag" type="string" required>
|
||||
The authentication tag to use for decryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
#### Returns (string)
|
||||
|
@ -10,24 +10,23 @@ From local development to production, Infisical SDKs provide the easiest way for
|
||||
- Fetch secrets on demand
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Node" href="https://github.com/Infisical/node-sdk-v2" icon="node" color="#68a063">
|
||||
Manage secrets for your Node application on demand
|
||||
<Card title="Node.js" href="https://github.com/Infisical/node-sdk-v2?tab=readme-ov-file#infisical-nodejs-sdk" icon="node" color="#68a063">
|
||||
Manage secrets for your Node application on demand
|
||||
</Card>
|
||||
<Card href="https://github.com/Infisical/python-sdk-official" title="Python" icon="python" color="#4c8abe">
|
||||
Manage secrets for your Python application on demand
|
||||
<Card href="https://github.com/Infisical/python-sdk-official?tab=readme-ov-file#infisical-python-sdk" title="Python" icon="python" color="#4c8abe">
|
||||
Manage secrets for your Python application on demand
|
||||
</Card>
|
||||
<Card href="https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-nodejs-sdk" title="Java" icon="java" color="#e41f23">
|
||||
Manage secrets for your Java application on demand
|
||||
<Card href="https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-java-sdk" title="Java" icon="java" color="#e41f23">
|
||||
Manage secrets for your Java application on demand
|
||||
</Card>
|
||||
<Card href="/sdks/languages/go" title="Go" icon="golang" color="#367B99">
|
||||
Manage secrets for your Go application on demand
|
||||
Manage secrets for your Go application on demand
|
||||
</Card>
|
||||
<Card href="/sdks/languages/csharp" title="C#" icon="bars" color="#368833">
|
||||
Manage secrets for your C#/.NET application on demand
|
||||
<Card href="https://github.com/Infisical/infisical-dotnet-sdk?tab=readme-ov-file#infisical-net-sdk" title=".NET" icon="bars" color="#368833">
|
||||
Manage secrets for your .NET application on demand
|
||||
</Card>
|
||||
|
||||
<Card href="/sdks/languages/ruby" title="Ruby" icon="diamond" color="#367B99">
|
||||
Manage secrets for your Ruby application on demand
|
||||
<Card href="/sdks/languages/ruby" title="Ruby" icon="diamond" color="#367B99">
|
||||
Manage secrets for your Ruby application on demand
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
|
@ -590,6 +590,17 @@ You can configure third-party app connections for re-use across Infisical Projec
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="GitLab OAuth Connection">
|
||||
<ParamField query="INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID" type="string" default="none" optional>
|
||||
The Application ID of your GitLab OAuth application.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET" type="string" default="none" optional>
|
||||
The Secret of your GitLab OAuth application.
|
||||
</ParamField>
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Native Secret Integrations
|
||||
|
||||
To help you sync secrets from Infisical to services such as Github and Gitlab, Infisical provides native integrations out of the box.
|
||||
|
@ -5,13 +5,13 @@ description: "Learn how to provision and configure Infisical instances programma
|
||||
|
||||
Infisical's Automated Bootstrapping feature enables you to provision and configure an Infisical instance without using the UI, allowing for complete automation through static configuration files, API calls, or CLI commands. This is especially valuable for enterprise environments where automated deployment and infrastructure-as-code practices are essential.
|
||||
|
||||
## Overview
|
||||
The bootstrapping workflow automates creating an admin user account, initializing an organization for the entire instance, establishing an **instance admin machine identity** with full administrative permissions, and returning the machine identity credentials for further automation.
|
||||
|
||||
The Automated Bootstrapping workflow automates the following processes:
|
||||
- Creating an admin user account
|
||||
- Initializing an organization for the entire instance
|
||||
- Establishing an **instance admin machine identity** with full administrative permissions
|
||||
- Returning the machine identity credentials for further automation
|
||||
## Prerequisites
|
||||
|
||||
- An Infisical instance launched with all required configuration variables
|
||||
- Access to the Infisical CLI or the ability to make API calls to the instance
|
||||
- Network connectivity to the Infisical instance
|
||||
|
||||
## Key Concepts
|
||||
|
||||
@ -20,15 +20,9 @@ The Automated Bootstrapping workflow automates the following processes:
|
||||

|
||||
- **Token Auth**: The instance admin machine identity uses [Token Auth](/documentation/platform/identities/token-auth), providing a JWT token that can be used directly to make authenticated requests to the Infisical API.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An Infisical instance launched with all required configuration variables
|
||||
- Access to the Infisical CLI or the ability to make API calls to the instance
|
||||
- Network connectivity to the Infisical instance
|
||||
|
||||
## Bootstrap Methods
|
||||
|
||||
You can bootstrap an Infisical instance using either the API or the CLI.
|
||||
You can bootstrap an Infisical instance using the API, CLI, or Helm chart.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Using the API">
|
||||
@ -51,6 +45,37 @@ You can bootstrap an Infisical instance using either the API or the CLI.
|
||||
-d '{"email":"admin@example.com","password":"your-secure-password","organization":"your-org-name"}' \
|
||||
http://your-infisical-instance.com/api/v1/admin/bootstrap
|
||||
```
|
||||
|
||||
### API Response Structure
|
||||
|
||||
The bootstrap process returns a JSON response with details about the created user, organization, and machine identity:
|
||||
|
||||
```json
|
||||
{
|
||||
"identity": {
|
||||
"credentials": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiZGIyMjQ3OTItZWQxOC00Mjc3LTlkYWUtNTdlNzUyMzE1ODU0IiwiaWRlbnRpdHlBY2Nlc3NUb2tlbklkIjoiZmVkZmZmMGEtYmU3Yy00NjViLWEwZWEtZjM5OTNjMTg4OGRlIiwiYXV0aFRva2VuVHlwZSI6ImlkZW50aXR5QWNjZXNzVG9rZW4iLCJpYXQiOjE3NDIzMjI0ODl9.mqcZZqIFqER1e9ubrQXp8FbzGYi8nqqZwfMvz09g-8Y"
|
||||
},
|
||||
"id": "db224792-ed18-4277-9dae-57e752315854",
|
||||
"name": "Instance Admin Identity"
|
||||
},
|
||||
"message": "Successfully bootstrapped instance",
|
||||
"organization": {
|
||||
"id": "b56bece0-42f5-4262-b25e-be7bf5f84957",
|
||||
"name": "dog",
|
||||
"slug": "dog-v-e5l"
|
||||
},
|
||||
"user": {
|
||||
"email": "admin@example.com",
|
||||
"firstName": "Admin",
|
||||
"id": "a418f355-c8da-453c-bbc8-6c07208eeb3c",
|
||||
"lastName": "User",
|
||||
"superAdmin": true,
|
||||
"username": "admin@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Using the CLI">
|
||||
Use the [Infisical CLI](/cli/commands/bootstrap) to bootstrap the instance and extract the token for immediate use in automation:
|
||||
@ -60,39 +85,126 @@ You can bootstrap an Infisical instance using either the API or the CLI.
|
||||
```
|
||||
|
||||
This example command pipes the output through `jq` to extract only the machine identity token, making it easy to capture and use directly in automation scripts or export as an environment variable for tools like Terraform.
|
||||
|
||||
### API Response Structure
|
||||
|
||||
The bootstrap process returns a JSON response with details about the created user, organization, and machine identity:
|
||||
|
||||
```json
|
||||
{
|
||||
"identity": {
|
||||
"credentials": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiZGIyMjQ3OTItZWQxOC00Mjc3LTlkYWUtNTdlNzUyMzE1ODU0IiwiaWRlbnRpdHlBY2Nlc3NUb2tlbklkIjoiZmVkZmZmMGEtYmU3Yy00NjViLWEwZWEtZjM5OTNjMTg4OGRlIiwiYXV0aFRva2VuVHlwZSI6ImlkZW50aXR5QWNjZXNzVG9rZW4iLCJpYXQiOjE3NDIzMjI0ODl9.mqcZZqIFqER1e9ubrQXp8FbzGYi8nqqZwfMvz09g-8Y"
|
||||
},
|
||||
"id": "db224792-ed18-4277-9dae-57e752315854",
|
||||
"name": "Instance Admin Identity"
|
||||
},
|
||||
"message": "Successfully bootstrapped instance",
|
||||
"organization": {
|
||||
"id": "b56bece0-42f5-4262-b25e-be7bf5f84957",
|
||||
"name": "dog",
|
||||
"slug": "dog-v-e5l"
|
||||
},
|
||||
"user": {
|
||||
"email": "admin@example.com",
|
||||
"firstName": "Admin",
|
||||
"id": "a418f355-c8da-453c-bbc8-6c07208eeb3c",
|
||||
"lastName": "User",
|
||||
"superAdmin": true,
|
||||
"username": "admin@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Helm Chart">
|
||||
When deploying Infisical using the official [Helm chart](/self-hosting/deployment-options/kubernetes-helm#kubernetes-via-helm-chart), you can enable automatic bootstrapping that runs as part of the deployment process. This eliminates the need to manually bootstrap the instance after deployment.
|
||||
|
||||
The bootstrapping process automatically generates a Kubernetes secret containing the instance admin token, which can then be referenced by Crossplane providers, Terraform operators, or other automation systems for further infrastructure provisioning and configuration.
|
||||
|
||||
### Configuration
|
||||
|
||||
Enable auto bootstrapping in your Helm values by setting `autoBootstrap.enabled: true` and providing the necessary configuration:
|
||||
|
||||
```yaml
|
||||
autoBootstrap:
|
||||
enabled: true
|
||||
organization: "My Organization"
|
||||
secretTemplate: '{"data":{"token":"{{.Identity.Credentials.Token}}"}}'
|
||||
|
||||
secretDestination:
|
||||
name: "infisical-bootstrap-secret"
|
||||
namespace: "default" # defaults to release namespace if not specified
|
||||
|
||||
credentialSecret:
|
||||
name: "infisical-bootstrap-credentials"
|
||||
```
|
||||
|
||||
You'll also need to create a secret containing the bootstrap credentials before deployment. The secret must contain `INFISICAL_ADMIN_EMAIL` and `INFISICAL_ADMIN_PASSWORD` keys:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic infisical-bootstrap-credentials \
|
||||
--from-literal=INFISICAL_ADMIN_EMAIL="admin@example.com" \
|
||||
--from-literal=INFISICAL_ADMIN_PASSWORD="your-secure-password" \
|
||||
--namespace=release-namespace
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
The Helm chart auto bootstrap feature:
|
||||
|
||||
1. **Post-Install Hook**: Runs automatically after the main Infisical deployment is complete
|
||||
2. **Readiness Check**: Uses an init container with curl to wait for Infisical to be ready by polling the `/api/status` endpoint
|
||||
3. **Bootstrap Execution**: Uses the Infisical CLI to bootstrap the instance
|
||||
4. **Kubernetes Secret Creation**: Creates a Kubernetes secret directly via the Kubernetes API using the rendered template
|
||||
5. **RBAC**: Automatically configures the necessary permissions (`get`, `create`, `update` on secrets) for the bootstrap job
|
||||
|
||||
### Template System
|
||||
|
||||
The `secretTemplate` field allows you to customize the data section of the created Kubernetes secret. The template has access to the full bootstrap response with the following available data fields:
|
||||
|
||||
- `{{ .Identity.Credentials.Token }}`: The admin machine identity token
|
||||
- `{{ .Identity.ID }}`: The identity ID
|
||||
- `{{ .Identity.Name }}`: The identity name
|
||||
- `{{ .Organization.ID }}`: The organization ID
|
||||
- `{{ .Organization.Name }}`: The organization name
|
||||
- `{{ .Organization.Slug }}`: The organization slug
|
||||
- `{{ .User.Email }}`: The admin user email
|
||||
- `{{ .User.ID }}`: The admin user ID
|
||||
- `{{ .User.FirstName }}`: The admin user first name
|
||||
- `{{ .User.LastName }}`: The admin user last name
|
||||
|
||||
The template also supports the `encodeBase64` function for base64 encoding values.
|
||||
|
||||
Example template for storing multiple values:
|
||||
|
||||
```yaml
|
||||
secretTemplate: |
|
||||
{
|
||||
"data": {
|
||||
"infisical_token": "{{ .Identity.Credentials.Token }}",
|
||||
"admin_email": "{{ .User.Email }}",
|
||||
"organization": "{{ .Organization.Name }}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
- **Zero-Touch Deployment**: Complete Infisical setup without manual intervention
|
||||
- **Infrastructure as Code**: Bootstrap configuration is versioned with your Helm values
|
||||
- **Secure Token Storage**: Admin identity credentials are immediately stored in Kubernetes secrets
|
||||
- **Integration Ready**: The created secret can be referenced by other applications or automation tools
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- The bootstrap job requires permissions to create secrets in the specified namespace
|
||||
- Bootstrap credentials should be stored securely and rotated regularly
|
||||
- The generated admin token has full instance privileges and should be protected accordingly
|
||||
- Consider using Kubernetes RBAC to restrict access to the generated secret
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## API Response Structure
|
||||
|
||||
The bootstrap process returns a JSON response with details about the created user, organization, and machine identity:
|
||||
|
||||
```json
|
||||
{
|
||||
"identity": {
|
||||
"credentials": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiZGIyMjQ3OTItZWQxOC00Mjc3LTlkYWUtNTdlNzUyMzE1ODU0IiwiaWRlbnRpdHlBY2Nlc3NUb2tlbklkIjoiZmVkZmZmMGEtYmU3Yy00NjViLWEwZWEtZjM5OTNjMTg4OGRlIiwiYXV0aFRva2VuVHlwZSI6ImlkZW50aXR5QWNjZXNzVG9rZW4iLCJpYXQiOjE3NDIzMjI0ODl9.mqcZZqIFqER1e9ubrQXp8FbzGYi8nqqZwfMvz09g-8Y"
|
||||
},
|
||||
"id": "db224792-ed18-4277-9dae-57e752315854",
|
||||
"name": "Instance Admin Identity"
|
||||
},
|
||||
"message": "Successfully bootstrapped instance",
|
||||
"organization": {
|
||||
"id": "b56bece0-42f5-4262-b25e-be7bf5f84957",
|
||||
"name": "dog",
|
||||
"slug": "dog-v-e5l"
|
||||
},
|
||||
"user": {
|
||||
"email": "admin@example.com",
|
||||
"firstName": "Admin",
|
||||
"id": "a418f355-c8da-453c-bbc8-6c07208eeb3c",
|
||||
"lastName": "User",
|
||||
"superAdmin": true,
|
||||
"username": "admin@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using the Instance Admin Machine Identity Token
|
||||
|
||||
The bootstrap process automatically creates a machine identity with Token Auth configured. The returned token has instance-level admin privileges (the highest level of access) and should be treated with the same security considerations as a root credential.
|
||||
|
@ -2,10 +2,9 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { MongoAbility, MongoQuery } from "@casl/ability";
|
||||
import {
|
||||
faAnglesUp,
|
||||
faArrowUpRightFromSquare,
|
||||
faDownLeftAndUpRightToCenter,
|
||||
faUpRightAndDownLeftFromCenter,
|
||||
faWindowRestore
|
||||
faWindowRestore,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
@ -23,8 +22,8 @@ import {
|
||||
} from "@xyflow/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Button, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||
import { Button, IconButton, Select, SelectItem, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionSet, ProjectPermissionSub } from "@app/context/ProjectPermissionContext";
|
||||
|
||||
import { AccessTreeSecretPathInput } from "./nodes/FolderNode/components/AccessTreeSecretPathInput";
|
||||
import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode";
|
||||
@ -36,15 +35,17 @@ import { ViewMode } from "./types";
|
||||
|
||||
export type AccessTreeProps = {
|
||||
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||
subject: ProjectPermissionSub;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const EdgeTypes = { base: BasePermissionEdge };
|
||||
|
||||
const NodeTypes = { role: RoleNode, folder: FolderNode, showMoreButton: ShowMoreButtonNode };
|
||||
|
||||
const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
const AccessTreeContent = ({ permissions, subject, onClose }: AccessTreeProps) => {
|
||||
const [selectedPath, setSelectedPath] = useState<string>("/");
|
||||
const accessTreeData = useAccessTree(permissions, selectedPath);
|
||||
const accessTreeData = useAccessTree(permissions, selectedPath, subject);
|
||||
const { edges, nodes, isLoading, viewMode, setViewMode, environment } = accessTreeData;
|
||||
const [initialRender, setInitialRender] = useState(true);
|
||||
|
||||
@ -78,32 +79,32 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
setInitialRender(true);
|
||||
}, [selectedPath, environment]);
|
||||
}, [selectedPath, environment, subject, viewMode]);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (initialRender) {
|
||||
timer = setTimeout(() => {
|
||||
goToRootNode();
|
||||
fitView({ duration: 500 });
|
||||
setInitialRender(false);
|
||||
}, 500);
|
||||
}, 50);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [nodes, edges, getViewport(), initialRender, goToRootNode]);
|
||||
}, [nodes, edges, getViewport(), initialRender, fitView]);
|
||||
|
||||
const handleToggleModalView = () =>
|
||||
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal));
|
||||
|
||||
const handleToggleUndockedView = () =>
|
||||
setViewMode((prev) => (prev === ViewMode.Undocked ? ViewMode.Docked : ViewMode.Undocked));
|
||||
const handleToggleView = () =>
|
||||
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Undocked : ViewMode.Modal));
|
||||
|
||||
const undockButtonLabel = `${viewMode === ViewMode.Undocked ? "Dock" : "Undock"} View`;
|
||||
const windowButtonLabel = `${viewMode === ViewMode.Modal ? "Dock" : "Expand"} View`;
|
||||
const expandButtonLabel = viewMode === ViewMode.Modal ? "Anchor View" : "Expand View";
|
||||
const hideButtonLabel = "Hide Access Tree";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"w-full",
|
||||
"mt-4 w-full",
|
||||
viewMode === ViewMode.Modal && "fixed inset-0 z-50 p-10",
|
||||
viewMode === ViewMode.Undocked &&
|
||||
"fixed bottom-4 left-20 z-50 h-[40%] w-[38%] min-w-[32rem] lg:w-[34%]"
|
||||
@ -130,7 +131,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
type="submit"
|
||||
className="h-10 rounded-r-none bg-mineshaft-700"
|
||||
leftIcon={<FontAwesomeIcon icon={faWindowRestore} />}
|
||||
onClick={handleToggleUndockedView}
|
||||
onClick={handleToggleView}
|
||||
>
|
||||
Undock
|
||||
</Button>
|
||||
@ -176,48 +177,62 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
<Spinner />
|
||||
</Panel>
|
||||
)}
|
||||
{viewMode !== ViewMode.Undocked && (
|
||||
<Panel position="top-left" className="flex gap-2">
|
||||
<Select
|
||||
value={environment}
|
||||
onValueChange={accessTreeData.setEnvironment}
|
||||
className="w-60"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
aria-label="Environment"
|
||||
>
|
||||
{Object.values(accessTreeData.environments).map((env) => (
|
||||
<SelectItem
|
||||
key={env.slug}
|
||||
value={env.slug}
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 truncate font-medium">{env.name}</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<AccessTreeSecretPathInput
|
||||
placeholder="Provide a path, default is /"
|
||||
environment={environment}
|
||||
value={selectedPath}
|
||||
onChange={setSelectedPath}
|
||||
/>
|
||||
</Panel>
|
||||
)}
|
||||
{viewMode !== ViewMode.Docked && (
|
||||
<Panel position="top-right" className="flex gap-1.5">
|
||||
{viewMode !== ViewMode.Undocked && (
|
||||
<AccessTreeSecretPathInput
|
||||
placeholder="Provide a path, default is /"
|
||||
environment={environment}
|
||||
value={selectedPath}
|
||||
onChange={setSelectedPath}
|
||||
/>
|
||||
)}
|
||||
<Tooltip position="bottom" align="center" content={undockButtonLabel}>
|
||||
<Panel position="top-right" className="flex gap-2">
|
||||
<Tooltip position="bottom" align="center" content={expandButtonLabel}>
|
||||
<IconButton
|
||||
className="ml-1 w-10 rounded"
|
||||
className="rounded p-2"
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={handleToggleUndockedView}
|
||||
ariaLabel={undockButtonLabel}
|
||||
onClick={handleToggleView}
|
||||
ariaLabel={expandButtonLabel}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
viewMode === ViewMode.Undocked
|
||||
? faArrowUpRightFromSquare
|
||||
? faUpRightAndDownLeftFromCenter
|
||||
: faWindowRestore
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip align="end" position="bottom" content={windowButtonLabel}>
|
||||
<Tooltip align="end" position="bottom" content={hideButtonLabel}>
|
||||
<IconButton
|
||||
className="w-10 rounded"
|
||||
className="rounded p-2"
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={handleToggleModalView}
|
||||
ariaLabel={windowButtonLabel}
|
||||
onClick={onClose}
|
||||
ariaLabel={hideButtonLabel}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
viewMode === ViewMode.Modal
|
||||
? faDownLeftAndUpRightToCenter
|
||||
: faUpRightAndDownLeftFromCenter
|
||||
}
|
||||
/>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Panel>
|
||||
@ -253,6 +268,9 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
};
|
||||
|
||||
export const AccessTree = (props: AccessTreeProps) => {
|
||||
const { subject } = props;
|
||||
if (!subject) return null;
|
||||
|
||||
return (
|
||||
<AccessTreeErrorBoundary {...props}>
|
||||
<AccessTreeProvider>
|
||||
|
@ -29,7 +29,7 @@ export type AccessTreeForm = { metadata: { key: string; value: string }[] };
|
||||
export const AccessTreeProvider: React.FC<AccessTreeProviderProps> = ({ children }) => {
|
||||
const [secretName, setSecretName] = useState("");
|
||||
const formMethods = useForm<AccessTreeForm>({ defaultValues: { metadata: [] } });
|
||||
const [viewMode, setViewMode] = useState(ViewMode.Docked);
|
||||
const [viewMode, setViewMode] = useState(ViewMode.Modal);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
|