Compare commits
124 Commits
approvals-
...
improve-se
Author | SHA1 | Date | |
---|---|---|---|
|
bf7801f8c1 | ||
|
753e9250d1 | ||
|
6b6b15f3ea | ||
|
040d93080f | ||
|
3d32882cef | ||
|
620c453b12 | ||
|
4d8d1f721c | ||
|
9e4267a5d8 | ||
|
40f95e0cc5 | ||
|
43cc398ef0 | ||
|
e99ea1f573 | ||
|
034f473e58 | ||
|
759220263f | ||
|
14e3f62b95 | ||
|
4d1462624b | ||
|
51657449af | ||
|
d89976802d | ||
|
1090a61162 | ||
|
0e11ff198c | ||
|
cdbc6f5619 | ||
|
78cb18ad0e | ||
|
42374a775d | ||
|
0269b58a3c | ||
|
ef4a316558 | ||
|
a676ce7c21 | ||
|
f475daf7a6 | ||
|
c8110c31ef | ||
|
a5c8c9c279 | ||
|
5860136494 | ||
|
3f3516b7ba | ||
|
06e26da684 | ||
|
bb70ff96d2 | ||
|
c019d57fb6 | ||
|
7854a5eea2 | ||
|
29636173ef | ||
|
4edfc1e0be | ||
|
61d4da49aa | ||
|
56187ec43e | ||
|
971ac26033 | ||
|
1f316a0b65 | ||
|
23d09c37b5 | ||
|
fc7c3022be | ||
|
5b65adedbb | ||
|
6faf9bf4bf | ||
|
b5998d7f22 | ||
|
6abbc1c54d | ||
|
85e5319981 | ||
|
50da0a753a | ||
|
6a5f2d0566 | ||
|
d93277155f | ||
|
cb905e5ee6 | ||
|
71261e7594 | ||
|
27e4f490d3 | ||
|
298c8705d7 | ||
|
edc4382a48 | ||
|
5baab76f2e | ||
|
f9f30efe03 | ||
|
12701bdf98 | ||
|
70967ac7b0 | ||
|
98b443da82 | ||
|
10f75c8e55 | ||
|
b226642853 | ||
|
933f837f64 | ||
|
7327698305 | ||
|
1dc59d0d41 | ||
|
13e067dc4f | ||
|
885d348f96 | ||
|
c71ee77503 | ||
|
14206de926 | ||
|
c73d64d784 | ||
|
913067f014 | ||
|
ff2ee989d6 | ||
|
200cefc1b2 | ||
|
721af0f26d | ||
|
aca6269920 | ||
|
a4074c9687 | ||
|
205ec61549 | ||
|
0d16f707c2 | ||
|
d3d5ead6ed | ||
|
1f05d6ea4d | ||
|
ff82af8358 | ||
|
a7da858694 | ||
|
b5c2f6e551 | ||
|
77226e0924 | ||
|
0cc4286f5f | ||
|
99144143ff | ||
|
efff841121 | ||
|
2f8d914ecb | ||
|
7dd28a5941 | ||
|
a89fccdc1f | ||
|
40ddd3b2a5 | ||
|
74d17a20a4 | ||
|
d537bd2f58 | ||
|
2f045be8a4 | ||
|
c5ee4810ad | ||
|
1dbda5876f | ||
|
d948923d95 | ||
|
fb1085744a | ||
|
ec22291aca | ||
|
00a07fd27c | ||
|
ec0e77cc5a | ||
|
16c49a9626 | ||
|
06ea809d60 | ||
|
12364005c1 | ||
|
98573e9e05 | ||
|
c1a4ca6203 | ||
|
21c2fd8542 | ||
|
b27bc8fc1b | ||
|
091115e6ba | ||
|
d9c055872d | ||
|
f73d18ddc7 | ||
|
eb47126f68 | ||
|
4750767268 | ||
|
b0ed772885 | ||
|
7fdab81b5f | ||
|
c17bf13f8c | ||
|
515e010065 | ||
|
2c46e8a2dc | ||
|
eebe3c164a | ||
|
d7acd7aef6 | ||
|
860b8efd7d | ||
|
6ca3fc5ad2 | ||
|
189af07ff5 | ||
|
caf7426f86 |
@@ -48,10 +48,12 @@ CLIENT_ID_HEROKU=
|
||||
CLIENT_ID_VERCEL=
|
||||
CLIENT_ID_NETLIFY=
|
||||
CLIENT_ID_GITHUB=
|
||||
CLIENT_ID_GITLAB=
|
||||
CLIENT_SECRET_HEROKU=
|
||||
CLIENT_SECRET_VERCEL=
|
||||
CLIENT_SECRET_NETLIFY=
|
||||
CLIENT_SECRET_GITHUB=
|
||||
CLIENT_SECRET_GITLAB=
|
||||
CLIENT_SLUG_VERCEL=
|
||||
|
||||
# Sentry (optional) for monitoring errors
|
||||
|
16
README.md
@@ -25,7 +25,7 @@
|
||||
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
|
||||
</a>
|
||||
<a href="https://cloudsmith.io/~infisical/repos/">
|
||||
<img src="https://img.shields.io/badge/Downloads-36.7k-orange" alt="Cloudsmith downloads" />
|
||||
<img src="https://img.shields.io/badge/Downloads-45.7k-orange" alt="Cloudsmith downloads" />
|
||||
</a>
|
||||
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
|
||||
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
|
||||
@@ -44,6 +44,7 @@
|
||||
<kbd>[<img title="Turkish" alt="Turkish language" src="https://cdn.staticaly.com/gh/hjnilsson/country-flags/master/svg/tr.svg" width="22">](i18n/README.tr.md)</kbd>
|
||||
<kbd>[<img title="Bahasa Indonesia" alt="Bahasa Indonesia language" src="https://cdn.staticaly.com/gh/hjnilsson/country-flags/master/svg/id.svg" width="22">](i18n/README.id.md)</kbd>
|
||||
<kbd>[<img title="Portuguese - Brazil" alt="Portuguese - Brazil" src="https://cdn.staticaly.com/gh/hjnilsson/country-flags/master/svg/br.svg" width="22">](i18n/README.pt-br.md)</kbd>
|
||||
<kbd>[<img title="Japanese" alt="Japanese language" src="https://cdn.staticaly.com/gh/hjnilsson/country-flags/master/svg/jp.svg" width="22">](i18n/README.ja.md)</kbd>
|
||||
|
||||
**[Infisical](https://infisical.com)** is an open source, end-to-end encrypted secret manager which you can use to centralize your API keys and configs. From Infisical, you can then distribute these secrets across your whole development lifecycle - from development to production . It's designed to be simple and take minutes to get going.
|
||||
|
||||
@@ -55,14 +56,13 @@
|
||||
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure
|
||||
- **[Infisical API](https://infisical.com/docs/api-reference/overview/introduction)** - manage secrets via HTTPS requests to the platform
|
||||
- **[Secret Versioning](https://infisical.com/docs/getting-started/dashboard/versioning)** to view the change history for any secret
|
||||
- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** to record every action taken in a project
|
||||
- **[Audit Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** to record every action taken in a project
|
||||
- **[Point-in-time Secrets Recovery](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** for rolling back to any snapshot of your secrets
|
||||
- **Role-based Access Controls** per environment
|
||||
- **2FA** (more options coming soon)
|
||||
- **Smart Security Alerts**
|
||||
- 🔜 **1-Click Deploy** to AWS
|
||||
- 🔜 **Automatic Secret Rotation**
|
||||
- 🔜 **Smart Security Alerts**
|
||||
- 🔜 **Secrets Rotation**
|
||||
- 🔜 **Slack & MS Teams** integrations
|
||||
|
||||
And more.
|
||||
@@ -157,7 +157,9 @@ We're currently setting the foundation and building [integrations](https://infis
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)
|
||||
<a href="https://infisical.com/docs/integrations/cicd/circleci?ref=github.com">
|
||||
✔️ CircleCI
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -168,7 +170,9 @@ We're currently setting the foundation and building [integrations](https://infis
|
||||
🔜 Digital Ocean
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 Azure
|
||||
<a href="https://infisical.com/docs/integrations/cloud/azure-key-vault?ref=github.com">
|
||||
✔️ Azure Key Vault
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
2
backend/environment.d.ts
vendored
@@ -22,10 +22,12 @@ declare global {
|
||||
CLIENT_ID_VERCEL: string;
|
||||
CLIENT_ID_NETLIFY: string;
|
||||
CLIENT_ID_GITHUB: string;
|
||||
CLIENT_ID_GITLAB: string;
|
||||
CLIENT_SECRET_HEROKU: string;
|
||||
CLIENT_SECRET_VERCEL: string;
|
||||
CLIENT_SECRET_NETLIFY: string;
|
||||
CLIENT_SECRET_GITHUB: string;
|
||||
CLIENT_SECRET_GITLAB: string;
|
||||
CLIENT_SLUG_VERCEL: string;
|
||||
POSTHOG_HOST: string;
|
||||
POSTHOG_PROJECT_API_KEY: string;
|
||||
|
9570
backend/package-lock.json
generated
@@ -87,7 +87,7 @@
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@types/swagger-jsdoc": "^6.0.1",
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.26.0",
|
||||
|
@@ -39,8 +39,7 @@ import {
|
||||
password as v1PasswordRouter,
|
||||
stripe as v1StripeRouter,
|
||||
integration as v1IntegrationRouter,
|
||||
integrationAuth as v1IntegrationAuthRouter,
|
||||
secretApprovalRequest as v1SecretApprovalRequest
|
||||
integrationAuth as v1IntegrationAuthRouter
|
||||
} from './routes/v1';
|
||||
import {
|
||||
signup as v2SignupRouter,
|
||||
@@ -60,7 +59,7 @@ import { healthCheck } from './routes/status';
|
||||
|
||||
import { getLogger } from './utils/logger';
|
||||
import { RouteNotFoundError } from './utils/errors';
|
||||
import { handleMongoInvalidDataError, requestErrorHandler } from './middleware/requestErrorHandler';
|
||||
import { requestErrorHandler } from './middleware/requestErrorHandler';
|
||||
|
||||
// patch async route params to handle Promise Rejections
|
||||
patchRouterParam();
|
||||
@@ -111,7 +110,6 @@ app.use('/api/v1/password', v1PasswordRouter);
|
||||
app.use('/api/v1/stripe', v1StripeRouter);
|
||||
app.use('/api/v1/integration', v1IntegrationRouter);
|
||||
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||
app.use('/api/v1/secrets-approval-request', v1SecretApprovalRequest)
|
||||
|
||||
// v2 routes
|
||||
app.use('/api/v2/signup', v2SignupRouter);
|
||||
@@ -138,9 +136,6 @@ app.use((req, res, next) => {
|
||||
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
|
||||
})
|
||||
|
||||
// handle mongo validation errors
|
||||
app.use(handleMongoInvalidDataError);
|
||||
|
||||
//* Error Handling Middleware (must be after all routing logic)
|
||||
app.use(requestErrorHandler)
|
||||
|
||||
|
@@ -17,16 +17,17 @@ const NODE_ENV = process.env.NODE_ENV! || 'production';
|
||||
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
|
||||
const LOKI_HOST = process.env.LOKI_HOST || undefined;
|
||||
const CLIENT_ID_AZURE = process.env.CLIENT_ID_AZURE!;
|
||||
const TENANT_ID_AZURE = process.env.TENANT_ID_AZURE!;
|
||||
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
|
||||
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
|
||||
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
|
||||
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
|
||||
const CLIENT_ID_GITLAB = process.env.CLIENT_ID_GITLAB!;
|
||||
const CLIENT_SECRET_AZURE = process.env.CLIENT_SECRET_AZURE!;
|
||||
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
|
||||
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
|
||||
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
|
||||
const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!;
|
||||
const CLIENT_SECRET_GITLAB = process.env.CLIENT_SECRET_GITLAB;
|
||||
const CLIENT_SLUG_VERCEL = process.env.CLIENT_SLUG_VERCEL!;
|
||||
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
|
||||
const POSTHOG_PROJECT_API_KEY =
|
||||
@@ -70,16 +71,17 @@ export {
|
||||
VERBOSE_ERROR_OUTPUT,
|
||||
LOKI_HOST,
|
||||
CLIENT_ID_AZURE,
|
||||
TENANT_ID_AZURE,
|
||||
CLIENT_ID_HEROKU,
|
||||
CLIENT_ID_VERCEL,
|
||||
CLIENT_ID_NETLIFY,
|
||||
CLIENT_ID_GITHUB,
|
||||
CLIENT_ID_GITLAB,
|
||||
CLIENT_SECRET_AZURE,
|
||||
CLIENT_SECRET_HEROKU,
|
||||
CLIENT_SECRET_VERCEL,
|
||||
CLIENT_SECRET_NETLIFY,
|
||||
CLIENT_SECRET_GITHUB,
|
||||
CLIENT_SECRET_GITLAB,
|
||||
CLIENT_SLUG_VERCEL,
|
||||
POSTHOG_HOST,
|
||||
POSTHOG_PROJECT_API_KEY,
|
||||
|
@@ -14,7 +14,6 @@ import * as stripeController from './stripeController';
|
||||
import * as userActionController from './userActionController';
|
||||
import * as userController from './userController';
|
||||
import * as workspaceController from './workspaceController';
|
||||
import * as secretApprovalController from './secretApprovalController';
|
||||
|
||||
export {
|
||||
authController,
|
||||
@@ -32,6 +31,5 @@ export {
|
||||
stripeController,
|
||||
userActionController,
|
||||
userController,
|
||||
workspaceController,
|
||||
secretApprovalController
|
||||
workspaceController
|
||||
};
|
||||
|
@@ -2,13 +2,16 @@ import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Integration,
|
||||
IntegrationAuth,
|
||||
Bot
|
||||
} from '../../models';
|
||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
|
||||
import { IntegrationService } from '../../services';
|
||||
import { getApps, revokeAccess } from '../../integrations';
|
||||
import {
|
||||
getApps,
|
||||
getTeams,
|
||||
revokeAccess
|
||||
} from '../../integrations';
|
||||
|
||||
/***
|
||||
* Return integration authorization with id [integrationAuthId]
|
||||
@@ -154,25 +157,54 @@ export const saveIntegrationAccessToken = async (
|
||||
* @returns
|
||||
*/
|
||||
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||
let apps;
|
||||
try {
|
||||
apps = await getApps({
|
||||
integrationAuth: req.integrationAuth,
|
||||
accessToken: req.accessToken,
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to get integration authorization applications",
|
||||
});
|
||||
}
|
||||
let apps;
|
||||
try {
|
||||
const teamId = req.query.teamId as string;
|
||||
|
||||
apps = await getApps({
|
||||
integrationAuth: req.integrationAuth,
|
||||
accessToken: req.accessToken,
|
||||
...teamId && { teamId }
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to get integration authorization applications",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
apps,
|
||||
});
|
||||
return res.status(200).send({
|
||||
apps
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of teams allowed for integration with integration authorization id [integrationAuthId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
|
||||
let teams;
|
||||
try {
|
||||
teams = await getTeams({
|
||||
integrationAuth: req.integrationAuth,
|
||||
accessToken: req.accessToken
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to get integration authorization teams"
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
teams
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete integration authorization with id [integrationAuthId]
|
||||
* @param req
|
||||
|
@@ -2,10 +2,7 @@ import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Integration,
|
||||
Workspace,
|
||||
Bot,
|
||||
BotKey
|
||||
Integration
|
||||
} from '../../models';
|
||||
import { EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
@@ -18,6 +15,7 @@ import { eventPushSecrets } from '../../events';
|
||||
*/
|
||||
export const createIntegration = async (req: Request, res: Response) => {
|
||||
let integration;
|
||||
|
||||
try {
|
||||
const {
|
||||
integrationAuthId,
|
||||
@@ -34,19 +32,19 @@ export const createIntegration = async (req: Request, res: Response) => {
|
||||
// TODO: validate [sourceEnvironment] and [targetEnvironment]
|
||||
|
||||
// initialize new integration after saving integration access token
|
||||
integration = await new Integration({
|
||||
workspace: req.integrationAuth.workspace._id,
|
||||
environment: sourceEnvironment,
|
||||
isActive,
|
||||
app,
|
||||
integration = await new Integration({
|
||||
workspace: req.integrationAuth.workspace._id,
|
||||
environment: sourceEnvironment,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
integration: req.integrationAuth.integration,
|
||||
integrationAuth: new Types.ObjectId(integrationAuthId)
|
||||
}).save();
|
||||
integration: req.integrationAuth.integration,
|
||||
integrationAuth: new Types.ObjectId(integrationAuthId)
|
||||
}).save();
|
||||
|
||||
if (integration) {
|
||||
// trigger event - push secrets
|
||||
|
@@ -1,320 +0,0 @@
|
||||
import { Request, Response } from 'express';
|
||||
import SecretApprovalRequest, { ApprovalStatus, ChangeType, IApprover, IRequestedChange } from '../../models/secretApprovalRequest';
|
||||
import { Builder, IBuilder } from "builder-pattern"
|
||||
import { secretObjectHasRequiredFields, validateSecrets } from '../../helpers/secret';
|
||||
import _ from 'lodash';
|
||||
import { SECRET_PERSONAL, SECRET_SHARED } from '../../variables';
|
||||
import { BadRequestError, ResourceNotFound, UnauthorizedRequestError } from '../../utils/errors';
|
||||
import { ISecret, Membership, Secret, Workspace } from '../../models';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
export const createApprovalRequest = async (req: Request, res: Response) => {
|
||||
const { workspaceId, environment, requestedChanges } = req.body;
|
||||
|
||||
// validate workspace
|
||||
const workspaceFromDB = await Workspace.findById(workspaceId)
|
||||
if (!workspaceFromDB) {
|
||||
throw ResourceNotFound()
|
||||
}
|
||||
|
||||
const environmentBelongsToWorkspace = _.some(workspaceFromDB.environments, { slug: environment })
|
||||
if (!environmentBelongsToWorkspace) {
|
||||
throw ResourceNotFound()
|
||||
}
|
||||
|
||||
// check for secret duplicates
|
||||
const hasSecretIdDuplicates = requestedChanges.length !== _.uniqBy(requestedChanges, 'modifiedSecretParentId').length;
|
||||
if (hasSecretIdDuplicates) {
|
||||
throw BadRequestError({ message: "Request cannot contain changes for duplicate secrets" })
|
||||
}
|
||||
|
||||
// ensure the workspace has approvers set
|
||||
if (!workspaceFromDB.approvers.length) {
|
||||
throw BadRequestError({ message: "There are no designated approvers for this project, you must set approvers first before making a request" })
|
||||
}
|
||||
|
||||
const approverIds = _.compact(_.map(workspaceFromDB.approvers, "userId"))
|
||||
const approversFormatted: IApprover[] = approverIds.map(id => {
|
||||
return { "userId": id, status: ApprovalStatus.PENDING }
|
||||
})
|
||||
|
||||
const listOfSecretIdsToModify = _.compact(_.map(requestedChanges, "modifiedSecretParentId"))
|
||||
|
||||
// Ensure that the user requesting changes for the set of secrets can indeed interact with said secrets
|
||||
if (listOfSecretIdsToModify.length > 0) {
|
||||
await validateSecrets({
|
||||
userId: req.user._id.toString(),
|
||||
secretIds: listOfSecretIdsToModify
|
||||
});
|
||||
}
|
||||
|
||||
const sanitizedRequestedChangesList: IRequestedChange[] = []
|
||||
requestedChanges.forEach((requestedChange: IRequestedChange) => {
|
||||
const secretDetailsIsValid = secretObjectHasRequiredFields(requestedChange.modifiedSecretDetails)
|
||||
if (!secretDetailsIsValid) {
|
||||
throw BadRequestError({ message: "One or more required fields are missing from your modified secret" })
|
||||
}
|
||||
|
||||
if (!requestedChange.modifiedSecretParentId && (requestedChange.type != ChangeType.DELETE.toString() && requestedChange.type != ChangeType.CREATE.toString())) {
|
||||
throw BadRequestError({ message: "modifiedSecretParentId can only be empty when secret change type is DELETE or CREATE" })
|
||||
}
|
||||
|
||||
sanitizedRequestedChangesList.push(Builder<IRequestedChange>()
|
||||
.modifiedSecretParentId(requestedChange.modifiedSecretParentId)
|
||||
.modifiedSecretDetails(requestedChange.modifiedSecretDetails)
|
||||
.approvers(approversFormatted)
|
||||
.type(requestedChange.type).build())
|
||||
});
|
||||
|
||||
const newApprovalRequest = await SecretApprovalRequest.create({
|
||||
workspace: workspaceId,
|
||||
requestedByUserId: req.user._id.toString(),
|
||||
environment: environment,
|
||||
requestedChanges: sanitizedRequestedChangesList
|
||||
})
|
||||
|
||||
const populatedNewApprovalRequest = await newApprovalRequest.populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
|
||||
|
||||
return res.send({ approvalRequest: populatedNewApprovalRequest });
|
||||
};
|
||||
|
||||
export const getAllApprovalRequestsForUser = async (req: Request, res: Response) => {
|
||||
const approvalRequests = await SecretApprovalRequest.find({
|
||||
requestedByUserId: req.user._id.toString()
|
||||
}).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
|
||||
.sort({ updatedAt: -1 })
|
||||
|
||||
res.send({ approvalRequests: approvalRequests })
|
||||
}
|
||||
|
||||
export const getAllApprovalRequestsThatRequireUserApproval = async (req: Request, res: Response) => {
|
||||
const approvalRequests = await SecretApprovalRequest.find({
|
||||
'requestedChanges.approvers.userId': req.user._id.toString()
|
||||
}).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
|
||||
.sort({ updatedAt: -1 })
|
||||
|
||||
res.send({ approvalRequests: approvalRequests })
|
||||
}
|
||||
|
||||
export const approveApprovalRequest = async (req: Request, res: Response) => {
|
||||
const { requestedChangeIds } = req.body;
|
||||
const { reviewId } = req.params
|
||||
|
||||
const approvalRequestFromDB = await SecretApprovalRequest.findById(reviewId)
|
||||
if (!approvalRequestFromDB) {
|
||||
throw ResourceNotFound()
|
||||
}
|
||||
|
||||
const requestedChangesFromDB: IRequestedChange[] = approvalRequestFromDB.requestedChanges
|
||||
const filteredChangesByIds = requestedChangesFromDB.filter(change => requestedChangeIds.includes(change._id.toString()))
|
||||
if (filteredChangesByIds.length != requestedChangeIds.length) {
|
||||
throw BadRequestError({ message: "All requestedChangeIds should exist in this approval request" })
|
||||
}
|
||||
|
||||
const changesThatRequireUserApproval = _.filter(filteredChangesByIds, change => {
|
||||
return _.some(change.approvers, approver => {
|
||||
return approver.userId.toString() == req.user._id.toString();
|
||||
});
|
||||
});
|
||||
|
||||
if (!changesThatRequireUserApproval.length) {
|
||||
throw UnauthorizedRequestError({ message: "Your approval is not required for this review" })
|
||||
}
|
||||
|
||||
if (changesThatRequireUserApproval.length != filteredChangesByIds.length) {
|
||||
throw BadRequestError({ message: "You may only request to approve changes that require your approval" })
|
||||
}
|
||||
|
||||
changesThatRequireUserApproval.forEach((requestedChange) => {
|
||||
const overallChangeStatus = requestedChange.status
|
||||
const currentLoggedInUserId = req.user._id.toString()
|
||||
if (overallChangeStatus == ApprovalStatus.PENDING.toString()) {
|
||||
requestedChange.approvers.forEach((approver) => {
|
||||
if (approver.userId.toString() == currentLoggedInUserId && approver.status == ApprovalStatus.PENDING.toString()) {
|
||||
approver.status = ApprovalStatus.APPROVED
|
||||
}
|
||||
})
|
||||
|
||||
let updateOverallStatusToApproved = true
|
||||
requestedChange.approvers.forEach((approver) => {
|
||||
if (approver.status != ApprovalStatus.APPROVED.toString()) {
|
||||
updateOverallStatusToApproved = false
|
||||
}
|
||||
})
|
||||
|
||||
if (updateOverallStatusToApproved) {
|
||||
requestedChange.status = ApprovalStatus.APPROVED
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const updatedApprovalRequest = await SecretApprovalRequest.findByIdAndUpdate(reviewId, {
|
||||
requestedChanges: requestedChangesFromDB
|
||||
}, { new: true }).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
|
||||
|
||||
res.send({ approvalRequest: updatedApprovalRequest })
|
||||
}
|
||||
|
||||
|
||||
export const rejectApprovalRequest = async (req: Request, res: Response) => {
|
||||
const { requestedChangeIds } = req.body;
|
||||
const { reviewId } = req.params
|
||||
|
||||
const approvalRequestFromDB = await SecretApprovalRequest.findById(reviewId)
|
||||
if (!approvalRequestFromDB) {
|
||||
throw ResourceNotFound()
|
||||
}
|
||||
|
||||
const requestedChangesFromDB: IRequestedChange[] = approvalRequestFromDB.requestedChanges
|
||||
const filteredChangesByIds = requestedChangesFromDB.filter(change => requestedChangeIds.includes(change._id.toString()))
|
||||
if (filteredChangesByIds.length != requestedChangeIds.length) {
|
||||
throw BadRequestError({ message: "All requestedChangeIds should exist in this approval request" })
|
||||
}
|
||||
|
||||
const changesThatRequireUserApproval = _.filter(filteredChangesByIds, change => {
|
||||
return _.some(change.approvers, approver => {
|
||||
return approver.userId.toString() == req.user._id.toString();
|
||||
});
|
||||
});
|
||||
|
||||
if (!changesThatRequireUserApproval.length) {
|
||||
throw UnauthorizedRequestError({ message: "Your approval is not required for this review" })
|
||||
}
|
||||
|
||||
if (changesThatRequireUserApproval.length != filteredChangesByIds.length) {
|
||||
throw BadRequestError({ message: "You may only request to reject changes that require your approval" })
|
||||
}
|
||||
|
||||
changesThatRequireUserApproval.forEach((requestedChange) => {
|
||||
const overallChangeStatus = requestedChange.status
|
||||
const currentLoggedInUserId = req.user._id.toString()
|
||||
if (overallChangeStatus == ApprovalStatus.PENDING.toString()) {
|
||||
requestedChange.approvers.forEach((approver) => {
|
||||
if (approver.userId.toString() == currentLoggedInUserId && approver.status == ApprovalStatus.PENDING.toString()) {
|
||||
approver.status = ApprovalStatus.REJECTED
|
||||
requestedChange.status = ApprovalStatus.REJECTED
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const updatedApprovalRequest = await SecretApprovalRequest.findByIdAndUpdate(reviewId, {
|
||||
requestedChanges: requestedChangesFromDB
|
||||
}, { new: true }).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
|
||||
|
||||
res.send({ approvalRequest: updatedApprovalRequest })
|
||||
};
|
||||
|
||||
export const mergeApprovalRequestSecrets = async (req: Request, res: Response) => {
|
||||
const { requestedChangeIds } = req.body;
|
||||
const { reviewId } = req.params
|
||||
|
||||
// only the user who requested the set of changes can merge it
|
||||
const approvalRequestFromDB = await SecretApprovalRequest.findOne({ _id: reviewId, requestedByUserId: req.user._id })
|
||||
if (!approvalRequestFromDB) {
|
||||
throw ResourceNotFound()
|
||||
}
|
||||
|
||||
// ensure that this user is a member of this workspace
|
||||
const membershipDetails = await Membership.find({ user: req.user._id, workspace: approvalRequestFromDB.workspace })
|
||||
if (!membershipDetails) {
|
||||
throw UnauthorizedRequestError()
|
||||
}
|
||||
|
||||
// filter not merged, approved, and change ids specified in this request
|
||||
const filteredChangesToMerge: IRequestedChange[] = approvalRequestFromDB.requestedChanges.filter(change => change.merged == false && change.status == ApprovalStatus.APPROVED && requestedChangeIds.includes(change._id.toString()))
|
||||
|
||||
if (filteredChangesToMerge.length != requestedChangeIds.length) {
|
||||
throw BadRequestError({ message: "One or more changes in this approval is either already merged/not approved or do not exist" })
|
||||
}
|
||||
|
||||
const secretsToCreate: ISecret[] = []
|
||||
const secretsToUpdate: any[] = []
|
||||
const secretsIdsToDelete: any[] = []
|
||||
const secretIdsToModify: any[] = []
|
||||
|
||||
filteredChangesToMerge.forEach((requestedChange: any) => {
|
||||
const overallChangeStatus = requestedChange.status
|
||||
const currentLoggedInUserId = req.user._id.toString()
|
||||
if (overallChangeStatus == ApprovalStatus.APPROVED.toString()) {
|
||||
if (ChangeType.CREATE.toString() == requestedChange.type) {
|
||||
const modifiedSecret = requestedChange.modifiedSecretDetails.toObject()
|
||||
|
||||
secretsToCreate.push({
|
||||
...modifiedSecret,
|
||||
user: requestedChange.modifiedSecretDetails.type === SECRET_PERSONAL ? currentLoggedInUserId : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
if (ChangeType.UPDATE.toString() == requestedChange.type) {
|
||||
const modifiedSecret = requestedChange.modifiedSecretDetails.toObject()
|
||||
secretIdsToModify.push(requestedChange.modifiedSecretParentId)
|
||||
|
||||
secretsToUpdate.push({
|
||||
filter: { _id: requestedChange.modifiedSecretParentId },
|
||||
update: {
|
||||
$set: {
|
||||
...modifiedSecret,
|
||||
user: requestedChange.modifiedSecretDetails.type === SECRET_PERSONAL ? currentLoggedInUserId : undefined,
|
||||
},
|
||||
$inc: {
|
||||
version: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if (ChangeType.DELETE.toString() == requestedChange.type) {
|
||||
secretsIdsToDelete.push({
|
||||
_id: requestedChange.modifiedSecretParentId.toString()
|
||||
})
|
||||
}
|
||||
|
||||
requestedChange.merged = true
|
||||
}
|
||||
})
|
||||
|
||||
// ensure all secrets that are to be updated exist
|
||||
const numSecretsFromDBThatRequireUpdate = await Secret.countDocuments({ _id: { $in: secretIdsToModify } });
|
||||
const numSecretsFromDBThatRequireDelete = await Secret.countDocuments({ _id: { $in: secretsIdsToDelete } });
|
||||
|
||||
if (numSecretsFromDBThatRequireUpdate != secretIdsToModify.length || numSecretsFromDBThatRequireDelete != secretsIdsToDelete.length) {
|
||||
throw BadRequestError({ message: "You cannot merge changes for secrets that no longer exist" })
|
||||
}
|
||||
|
||||
// Add add CRUD operations into a single list of operations
|
||||
const allOperationsForBulkWrite: any[] = [];
|
||||
|
||||
for (const updateStatement of secretsToUpdate) {
|
||||
allOperationsForBulkWrite.push({ updateOne: updateStatement });
|
||||
}
|
||||
|
||||
for (const secretId of secretsIdsToDelete) {
|
||||
allOperationsForBulkWrite.push({ deleteOne: { filter: { _id: secretId } } });
|
||||
}
|
||||
|
||||
for (const createStatement of secretsToCreate) {
|
||||
allOperationsForBulkWrite.push({ insertOne: { document: createStatement } });
|
||||
}
|
||||
|
||||
// start transaction
|
||||
const session = await mongoose.startSession();
|
||||
session.startTransaction();
|
||||
|
||||
try {
|
||||
await Secret.bulkWrite(allOperationsForBulkWrite);
|
||||
await SecretApprovalRequest.updateOne({ _id: reviewId, 'requestedChanges._id': { $in: requestedChangeIds } },
|
||||
{ $set: { 'requestedChanges.$.merged': true } })
|
||||
|
||||
const updatedApproval = await SecretApprovalRequest.findById(reviewId).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
|
||||
|
||||
res.send(updatedApproval)
|
||||
} catch (error) {
|
||||
await session.abortTransaction();
|
||||
throw error
|
||||
} finally {
|
||||
session.endSession();
|
||||
}
|
||||
|
||||
};
|
@@ -16,8 +16,7 @@ import {
|
||||
} from "../../helpers/workspace";
|
||||
import { addMemberships } from "../../helpers/membership";
|
||||
import { ADMIN } from "../../variables";
|
||||
import { BadRequestError, ResourceNotFound, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import _ from "lodash";
|
||||
|
||||
/**
|
||||
* Return public keys of members of workspace with id [workspaceId]
|
||||
* @param req
|
||||
@@ -304,112 +303,6 @@ export const getWorkspaceIntegrationAuthorizations = async (
|
||||
});
|
||||
};
|
||||
|
||||
export const addApproverForWorkspaceAndEnvironment = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
interface Approver {
|
||||
environment: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
const { workspaceId } = req.params;
|
||||
const { approvers }: { approvers: Approver[] } = req.body;
|
||||
|
||||
const workspaceFromDB = await Workspace.findById(workspaceId)
|
||||
|
||||
if (!workspaceFromDB) {
|
||||
throw ResourceNotFound()
|
||||
}
|
||||
|
||||
const allAvailableWorkspaceEnvironments = _.map(workspaceFromDB.environments, 'slug');
|
||||
const environmentsFromApprovers = _.map(approvers, "environment")
|
||||
const filteredApprovers = environmentsFromApprovers.map(environment => allAvailableWorkspaceEnvironments.includes(environment))
|
||||
|
||||
// validate environments
|
||||
if (filteredApprovers.length != environmentsFromApprovers.length) {
|
||||
const err = `One or more environments set for approver(s) is invalid`
|
||||
throw BadRequestError({ message: err })
|
||||
}
|
||||
|
||||
const approverIds = _.map(approvers, "userId")
|
||||
|
||||
// validate approvers membership
|
||||
const approversMemberships = await Membership.find({
|
||||
workspace: workspaceId,
|
||||
user: { $in: approverIds }
|
||||
})
|
||||
|
||||
if (!approversMemberships) {
|
||||
throw ResourceNotFound()
|
||||
}
|
||||
|
||||
if (approversMemberships.length != approverIds.length) {
|
||||
throw UnauthorizedRequestError({ message: "Approvers must be apart of the workspace they are being added to" })
|
||||
}
|
||||
|
||||
const updatedWorkspace = await Workspace.findByIdAndUpdate(workspaceId,
|
||||
{
|
||||
$addToSet: {
|
||||
approvers: {
|
||||
$each: approvers,
|
||||
}
|
||||
}
|
||||
}, { new: true })
|
||||
|
||||
return res.json(updatedWorkspace)
|
||||
};
|
||||
|
||||
|
||||
export const removeApproverForWorkspaceAndEnvironment = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
interface Approver {
|
||||
environment: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
const { workspaceId } = req.params;
|
||||
const { approvers }: { approvers: Approver[] } = req.body;
|
||||
|
||||
const workspaceFromDB = await Workspace.findById(workspaceId)
|
||||
|
||||
if (!workspaceFromDB) {
|
||||
throw ResourceNotFound()
|
||||
}
|
||||
|
||||
const allAvailableWorkspaceEnvironments = _.map(workspaceFromDB.environments, 'slug');
|
||||
const environmentsFromApprovers = _.map(approvers, "environment")
|
||||
const filteredApprovers = environmentsFromApprovers.map(environment => allAvailableWorkspaceEnvironments.includes(environment))
|
||||
|
||||
// validate environments
|
||||
if (filteredApprovers.length != environmentsFromApprovers.length) {
|
||||
const err = `One or more environments set for approver(s) is invalid`
|
||||
throw BadRequestError({ message: err })
|
||||
}
|
||||
|
||||
const approverIds = _.map(approvers, "userId")
|
||||
|
||||
// validate approvers membership
|
||||
const approversMemberships = await Membership.find({
|
||||
workspace: workspaceId,
|
||||
user: { $in: approverIds }
|
||||
})
|
||||
|
||||
if (!approversMemberships) {
|
||||
throw ResourceNotFound()
|
||||
}
|
||||
|
||||
if (approversMemberships.length != approverIds.length) {
|
||||
throw UnauthorizedRequestError({ message: "Approvers must be apart of the workspace they are being added to" })
|
||||
}
|
||||
|
||||
const updatedWorkspace = await Workspace.findByIdAndUpdate(workspaceId, { $pullAll: { approvers: approvers } }, { new: true })
|
||||
|
||||
return res.json(updatedWorkspace)
|
||||
};
|
||||
|
||||
/**
|
||||
* Return service service tokens for workspace [workspaceId] belonging to user
|
||||
* @param req
|
||||
|
@@ -22,7 +22,7 @@ import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } fro
|
||||
import Tag from '../../models/tag';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
BatchSecretRequest,
|
||||
BatchSecretRequest,
|
||||
BatchSecret
|
||||
} from '../../types/secret';
|
||||
|
||||
@@ -41,13 +41,13 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
requests: BatchSecretRequest[];
|
||||
}= req.body;
|
||||
|
||||
} = req.body;
|
||||
|
||||
const createSecrets: BatchSecret[] = [];
|
||||
const updateSecrets: BatchSecret[] = [];
|
||||
const deleteSecrets: Types.ObjectId[] = [];
|
||||
const actions: IAction[] = [];
|
||||
|
||||
|
||||
requests.forEach((request) => {
|
||||
switch (request.method) {
|
||||
case 'POST':
|
||||
@@ -70,7 +70,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// handle create secrets
|
||||
let createdSecrets: ISecret[] = [];
|
||||
if (createSecrets.length > 0) {
|
||||
@@ -109,18 +109,18 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// handle update secrets
|
||||
let updatedSecrets: ISecret[] = [];
|
||||
if (updateSecrets.length > 0 && req.secrets) {
|
||||
// construct object containing all secrets
|
||||
let listedSecretsObj: {
|
||||
[key: string]: {
|
||||
[key: string]: {
|
||||
version: number;
|
||||
type: string;
|
||||
}
|
||||
} = {};
|
||||
|
||||
|
||||
listedSecretsObj = req.secrets.reduce((obj: any, secret: ISecret) => ({
|
||||
...obj,
|
||||
[secret._id.toString()]: secret
|
||||
@@ -140,7 +140,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
}));
|
||||
|
||||
await Secret.bulkWrite(updateOperations);
|
||||
|
||||
|
||||
const secretVersions = updateSecrets.map((u) => ({
|
||||
secret: new Types.ObjectId(u._id),
|
||||
version: listedSecretsObj[u._id.toString()].version,
|
||||
@@ -227,7 +227,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (actions.length > 0) {
|
||||
// (EE) create (audit) log
|
||||
await EELogService.createLog({
|
||||
@@ -250,7 +250,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId
|
||||
});
|
||||
|
||||
|
||||
const resObj: { [key: string]: ISecret[] | string[] } = {}
|
||||
|
||||
if (createSecrets.length > 0) {
|
||||
@@ -260,11 +260,11 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
if (updateSecrets.length > 0) {
|
||||
resObj['updatedSecrets'] = updatedSecrets;
|
||||
}
|
||||
|
||||
|
||||
if (deleteSecrets.length > 0) {
|
||||
resObj['deletedSecrets'] = deleteSecrets.map((d) => d.toString());
|
||||
}
|
||||
|
||||
|
||||
return res.status(200).send(resObj);
|
||||
}
|
||||
|
||||
@@ -358,9 +358,25 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
const newlyCreatedSecrets = await Secret.insertMany(
|
||||
listOfSecretsToCreate.map(({
|
||||
const secretsToInsert: ISecret[] = listOfSecretsToCreate.map(({
|
||||
type,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
tags
|
||||
}: secretsToCreateType) => {
|
||||
return ({
|
||||
version: 1,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
type,
|
||||
user: type === SECRET_PERSONAL ? req.user : undefined,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
@@ -371,26 +387,10 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
tags
|
||||
}: secretsToCreateType) => {
|
||||
return ({
|
||||
version: 1,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
type,
|
||||
user: type === SECRET_PERSONAL ? req.user : undefined,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
tags
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
})
|
||||
|
||||
const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject());
|
||||
|
||||
setTimeout(async () => {
|
||||
// trigger event - push secrets
|
||||
@@ -953,6 +953,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
||||
const toDelete = req.secrets.map((s: any) => s._id);
|
||||
|
||||
|
@@ -17,7 +17,35 @@ import { ABILITY_READ } from '../../variables/organization';
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).json(req.serviceTokenData);
|
||||
export const getServiceTokenData = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return Infisical Token data'
|
||||
#swagger.description = 'Return Infisical Token data'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"serviceTokenData": {
|
||||
"type": "object",
|
||||
$ref: "#/components/schemas/ServiceTokenData",
|
||||
"description": "Details of service token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return res.status(200).json(req.serviceTokenData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new service token data for workspace with id [workspaceId] and
|
||||
@@ -28,6 +56,7 @@ export const getServiceTokenData = async (req: Request, res: Response) => res.st
|
||||
*/
|
||||
export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
let serviceToken, serviceTokenData;
|
||||
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
@@ -36,7 +65,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
expiresIn
|
||||
expiresIn,
|
||||
permissions
|
||||
} = req.body;
|
||||
|
||||
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ)
|
||||
@@ -59,7 +89,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
secretHash,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag
|
||||
tag,
|
||||
permissions
|
||||
}).save();
|
||||
|
||||
// return service token data without sensitive data
|
||||
|
@@ -41,7 +41,7 @@ export const getMe = async (req: Request, res: Response) => {
|
||||
try {
|
||||
user = await User
|
||||
.findById(req.user._id)
|
||||
.select('+publicKey +encryptedPrivateKey +iv +tag');
|
||||
.select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
|
@@ -2,6 +2,7 @@ import jwt from 'jsonwebtoken';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import bcrypt from 'bcrypt';
|
||||
import {
|
||||
IUser,
|
||||
User,
|
||||
ServiceTokenData,
|
||||
APIKeyData
|
||||
@@ -148,7 +149,10 @@ const getAuthSTDPayload = async ({
|
||||
|
||||
serviceTokenData = await ServiceTokenData
|
||||
.findById(TOKEN_IDENTIFIER)
|
||||
.select('+encryptedKey +iv +tag').populate('user');
|
||||
.select('+encryptedKey +iv +tag')
|
||||
.populate<{user: IUser}>('user');
|
||||
|
||||
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
||||
|
||||
} catch (err) {
|
||||
throw UnauthorizedRequestError({
|
||||
|
@@ -229,7 +229,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
|
||||
// access token is expired
|
||||
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
|
||||
accessToken = await exchangeRefresh({
|
||||
integration: integrationAuth.integration,
|
||||
integrationAuth,
|
||||
refreshToken
|
||||
});
|
||||
}
|
||||
|
@@ -267,7 +267,7 @@ const v1PushSecrets = async ({
|
||||
|
||||
if (toAdd.length > 0) {
|
||||
// add secrets
|
||||
const newSecrets = await Secret.insertMany(
|
||||
const newSecrets: ISecret[] = (await Secret.insertMany(
|
||||
toAdd.map((s, idx) => {
|
||||
const obj: any = {
|
||||
version: 1,
|
||||
@@ -294,7 +294,7 @@ const v1PushSecrets = async ({
|
||||
|
||||
return obj;
|
||||
})
|
||||
);
|
||||
)).map((insertedSecret) => insertedSecret.toObject());
|
||||
|
||||
// (EE) add secret versions for new secrets
|
||||
EESecretService.addSecretVersions({
|
||||
@@ -713,27 +713,10 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
||||
return reformatedSecrets;
|
||||
};
|
||||
|
||||
const secretObjectHasRequiredFields = (secretObject: ISecret) => {
|
||||
if (!secretObject.type ||
|
||||
!(secretObject.type === SECRET_PERSONAL || secretObject.type === SECRET_SHARED) ||
|
||||
!secretObject.secretKeyCiphertext ||
|
||||
!secretObject.secretKeyIV ||
|
||||
!secretObject.secretKeyTag ||
|
||||
(typeof secretObject.secretValueCiphertext !== 'string') ||
|
||||
!secretObject.secretValueIV ||
|
||||
!secretObject.secretValueTag) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
validateSecrets,
|
||||
v1PushSecrets,
|
||||
v2PushSecrets,
|
||||
pullSecrets,
|
||||
reformatPullSecrets,
|
||||
secretObjectHasRequiredFields
|
||||
reformatPullSecrets
|
||||
};
|
||||
|
@@ -10,11 +10,13 @@ import {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_TRAVISCI,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_GITLAB_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
@@ -23,26 +25,30 @@ import {
|
||||
INTEGRATION_TRAVISCI_API_URL,
|
||||
} from "../variables";
|
||||
|
||||
interface App {
|
||||
name: string;
|
||||
appId?: string;
|
||||
owner?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of names of apps for integration named [integration]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integration - name of integration
|
||||
* @param {String} obj.accessToken - access token for integration
|
||||
* @param {String} obj.teamId - (optional) id of team for getting integration apps (used for integrations like GitLab)
|
||||
* @returns {Object[]} apps - names of integration apps
|
||||
* @returns {String} apps.name - name of integration app
|
||||
*/
|
||||
const getApps = async ({
|
||||
integrationAuth,
|
||||
accessToken,
|
||||
teamId
|
||||
}: {
|
||||
integrationAuth: IIntegrationAuth;
|
||||
accessToken: string;
|
||||
teamId?: string;
|
||||
}) => {
|
||||
interface App {
|
||||
name: string;
|
||||
appId?: string;
|
||||
owner?: string;
|
||||
}
|
||||
|
||||
let apps: App[] = [];
|
||||
try {
|
||||
@@ -77,6 +83,12 @@ const getApps = async ({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
apps = await getAppsGitlab({
|
||||
accessToken,
|
||||
teamId
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_RENDER:
|
||||
apps = await getAppsRender({
|
||||
accessToken,
|
||||
@@ -190,21 +202,40 @@ const getAppsVercel = async ({
|
||||
* @returns {String} apps.name - name of Netlify site
|
||||
*/
|
||||
const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
|
||||
let apps;
|
||||
const apps: any = [];
|
||||
try {
|
||||
const res = (
|
||||
await request.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
|
||||
let page = 1;
|
||||
const perPage = 10;
|
||||
let hasMorePages = true;
|
||||
|
||||
// paginate through all sites
|
||||
while (hasMorePages) {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
per_page: String(perPage)
|
||||
});
|
||||
|
||||
const { data } = await request.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
})
|
||||
).data;
|
||||
});
|
||||
|
||||
data.map((a: any) => {
|
||||
apps.push({
|
||||
name: a.name,
|
||||
appId: a.site_id
|
||||
});
|
||||
});
|
||||
|
||||
if (data.length < perPage) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
|
||||
apps = res.map((a: any) => ({
|
||||
name: a.name,
|
||||
appId: a.site_id,
|
||||
}));
|
||||
page++;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
@@ -217,9 +248,9 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
|
||||
/**
|
||||
* Return list of repositories for Github integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - access token for Netlify API
|
||||
* @returns {Object[]} apps - names of Netlify sites
|
||||
* @returns {String} apps.name - name of Netlify site
|
||||
* @param {String} obj.accessToken - access token for Github API
|
||||
* @returns {Object[]} apps - names of Github sites
|
||||
* @returns {String} apps.name - name of Github site
|
||||
*/
|
||||
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
|
||||
let apps;
|
||||
@@ -401,4 +432,117 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
|
||||
return apps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of repositories for GitLab integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - access token for GitLab API
|
||||
* @returns {Object[]} apps - names of GitLab sites
|
||||
* @returns {String} apps.name - name of GitLab site
|
||||
*/
|
||||
const getAppsGitlab = async ({
|
||||
accessToken,
|
||||
teamId
|
||||
}: {
|
||||
accessToken: string;
|
||||
teamId?: string;
|
||||
}) => {
|
||||
const apps: App[] = [];
|
||||
|
||||
let page = 1;
|
||||
const perPage = 10;
|
||||
let hasMorePages = true;
|
||||
try {
|
||||
|
||||
if (teamId) {
|
||||
// case: fetch projects for group with id [teamId] in GitLab
|
||||
|
||||
while (hasMorePages) {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
per_page: String(perPage)
|
||||
});
|
||||
|
||||
const { data } = (
|
||||
await request.get(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
data.map((a: any) => {
|
||||
apps.push({
|
||||
name: a.name,
|
||||
appId: a.id
|
||||
});
|
||||
});
|
||||
|
||||
if (data.length < perPage) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
} else {
|
||||
// case: fetch projects for individual in GitLab
|
||||
|
||||
const { id } = (
|
||||
await request.get(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/user`,
|
||||
{
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
).data;
|
||||
|
||||
while (hasMorePages) {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
per_page: String(perPage)
|
||||
});
|
||||
|
||||
const { data } = (
|
||||
await request.get(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
data.map((a: any) => {
|
||||
apps.push({
|
||||
name: a.name,
|
||||
appId: a.id
|
||||
});
|
||||
});
|
||||
|
||||
if (data.length < perPage) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get GitLab projects");
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
export { getApps };
|
||||
|
@@ -6,11 +6,13 @@ import {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
INTEGRATION_GITHUB_TOKEN_URL
|
||||
INTEGRATION_GITHUB_TOKEN_URL,
|
||||
INTEGRATION_GITLAB_TOKEN_URL
|
||||
} from '../variables';
|
||||
import {
|
||||
SITE_URL,
|
||||
@@ -18,11 +20,13 @@ import {
|
||||
CLIENT_ID_VERCEL,
|
||||
CLIENT_ID_NETLIFY,
|
||||
CLIENT_ID_GITHUB,
|
||||
CLIENT_ID_GITLAB,
|
||||
CLIENT_SECRET_AZURE,
|
||||
CLIENT_SECRET_HEROKU,
|
||||
CLIENT_SECRET_VERCEL,
|
||||
CLIENT_SECRET_NETLIFY,
|
||||
CLIENT_SECRET_GITHUB
|
||||
CLIENT_SECRET_GITHUB,
|
||||
CLIENT_SECRET_GITLAB,
|
||||
} from '../config';
|
||||
|
||||
interface ExchangeCodeAzureResponse {
|
||||
@@ -66,6 +70,15 @@ interface ExchangeCodeGithubResponse {
|
||||
token_type: string;
|
||||
}
|
||||
|
||||
interface ExchangeCodeGitlabResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
|
||||
* code-token exchange for integration named [integration]
|
||||
@@ -114,6 +127,10 @@ const exchangeCode = async ({
|
||||
code
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
obj = await exchangeCodeGitlab({
|
||||
code
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
@@ -151,7 +168,7 @@ const exchangeCodeAzure = async ({
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + res.expires_in
|
||||
);
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Azure');
|
||||
@@ -341,4 +358,53 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Gitlab
|
||||
* code-token exchange
|
||||
* @param {Object} obj1
|
||||
* @param {Object} obj1.code - code for code-token exchange
|
||||
* @returns {Object} obj2
|
||||
* @returns {String} obj2.accessToken - access token for Gitlab API
|
||||
* @returns {String} obj2.refreshToken - refresh token for Gitlab API
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeGitlab = async ({ code }: { code: string }) => {
|
||||
let res: ExchangeCodeGitlabResponse;
|
||||
const accessExpiresAt = new Date();
|
||||
|
||||
try {
|
||||
res = (
|
||||
await request.post(
|
||||
INTEGRATION_GITLAB_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_id: CLIENT_ID_GITLAB,
|
||||
client_secret: CLIENT_SECRET_GITLAB,
|
||||
redirect_uri: `${SITE_URL}/integrations/gitlab/oauth2/callback`
|
||||
} as any),
|
||||
{
|
||||
headers: {
|
||||
"Accept-Encoding": "application/json",
|
||||
}
|
||||
}
|
||||
)
|
||||
).data;
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + res.expires_in
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Gitlab');
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accessExpiresAt
|
||||
};
|
||||
}
|
||||
|
||||
export { exchangeCode };
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { exchangeCode } from './exchange';
|
||||
import { exchangeRefresh } from './refresh';
|
||||
import { getApps } from './apps';
|
||||
import { getTeams } from './teams';
|
||||
import { syncSecrets } from './sync';
|
||||
import { revokeAccess } from './revoke';
|
||||
|
||||
@@ -8,6 +9,7 @@ export {
|
||||
exchangeCode,
|
||||
exchangeRefresh,
|
||||
getApps,
|
||||
getTeams,
|
||||
syncSecrets,
|
||||
revokeAccess
|
||||
}
|
@@ -1,16 +1,29 @@
|
||||
import request from '../config/request';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU } from '../variables';
|
||||
import {
|
||||
IIntegrationAuth
|
||||
} from '../models';
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_GITLAB,
|
||||
} from '../variables';
|
||||
import {
|
||||
SITE_URL,
|
||||
CLIENT_ID_AZURE,
|
||||
CLIENT_ID_GITLAB,
|
||||
CLIENT_SECRET_AZURE,
|
||||
CLIENT_SECRET_HEROKU
|
||||
CLIENT_SECRET_HEROKU,
|
||||
CLIENT_SECRET_GITLAB
|
||||
} from '../config';
|
||||
import {
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_TOKEN_URL
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_GITLAB_TOKEN_URL
|
||||
} from '../variables';
|
||||
import {
|
||||
IntegrationService
|
||||
} from '../services';
|
||||
|
||||
interface RefreshTokenAzureResponse {
|
||||
token_type: string;
|
||||
@@ -21,6 +34,23 @@ interface RefreshTokenAzureResponse {
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
interface RefreshTokenHerokuResponse {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
token_type: string;
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
interface RefreshTokenGitLabResponse {
|
||||
token_type: string;
|
||||
scope: string;
|
||||
expires_in: number;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for integration
|
||||
* named [integration]
|
||||
@@ -29,33 +59,61 @@ interface RefreshTokenAzureResponse {
|
||||
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
|
||||
*/
|
||||
const exchangeRefresh = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
refreshToken
|
||||
}: {
|
||||
integration: string;
|
||||
integrationAuth: IIntegrationAuth;
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
let accessToken;
|
||||
|
||||
interface TokenDetails {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
accessExpiresAt: Date;
|
||||
}
|
||||
|
||||
let tokenDetails: TokenDetails;
|
||||
try {
|
||||
switch (integration) {
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_AZURE_KEY_VAULT:
|
||||
accessToken = await exchangeRefreshAzure({
|
||||
tokenDetails = await exchangeRefreshAzure({
|
||||
refreshToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_HEROKU:
|
||||
accessToken = await exchangeRefreshHeroku({
|
||||
tokenDetails = await exchangeRefreshHeroku({
|
||||
refreshToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
tokenDetails = await exchangeRefreshGitLab({
|
||||
refreshToken
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error('Failed to exchange token for incompatible integration');
|
||||
}
|
||||
|
||||
if (tokenDetails?.accessToken && tokenDetails?.refreshToken && tokenDetails?.accessExpiresAt) {
|
||||
await IntegrationService.setIntegrationAuthAccess({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
accessId: null,
|
||||
accessToken: tokenDetails.accessToken,
|
||||
accessExpiresAt: tokenDetails.accessExpiresAt
|
||||
});
|
||||
|
||||
await IntegrationService.setIntegrationAuthRefresh({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
refreshToken: tokenDetails.refreshToken
|
||||
});
|
||||
}
|
||||
|
||||
return tokenDetails.accessToken;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get new OAuth2 access token');
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -71,7 +129,8 @@ const exchangeRefreshAzure = async ({
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
try {
|
||||
const res: RefreshTokenAzureResponse = (await request.post(
|
||||
const accessExpiresAt = new Date();
|
||||
const { data }: { data: RefreshTokenAzureResponse } = await request.post(
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
client_id: CLIENT_ID_AZURE,
|
||||
@@ -80,9 +139,17 @@ const exchangeRefreshAzure = async ({
|
||||
grant_type: 'refresh_token',
|
||||
client_secret: CLIENT_SECRET_AZURE
|
||||
} as any)
|
||||
)).data;
|
||||
);
|
||||
|
||||
return res.access_token;
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + data.expires_in
|
||||
);
|
||||
|
||||
return ({
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
accessExpiresAt
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
@@ -102,10 +169,13 @@ const exchangeRefreshHeroku = async ({
|
||||
}: {
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
|
||||
let accessToken;
|
||||
try {
|
||||
const res = await request.post(
|
||||
const accessExpiresAt = new Date();
|
||||
const {
|
||||
data
|
||||
}: {
|
||||
data: RefreshTokenHerokuResponse
|
||||
} = await request.post(
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
@@ -114,14 +184,69 @@ const exchangeRefreshHeroku = async ({
|
||||
} as any)
|
||||
);
|
||||
|
||||
accessToken = res.data.access_token;
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + data.expires_in
|
||||
);
|
||||
|
||||
return ({
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
accessExpiresAt
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to refresh OAuth2 access token for Heroku');
|
||||
}
|
||||
};
|
||||
|
||||
return accessToken;
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||
* GitLab integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.refreshToken - refresh token to use to get new access token for GitLab
|
||||
* @returns
|
||||
*/
|
||||
const exchangeRefreshGitLab = async ({
|
||||
refreshToken
|
||||
}: {
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
try {
|
||||
const accessExpiresAt = new Date();
|
||||
const {
|
||||
data
|
||||
}: {
|
||||
data: RefreshTokenGitLabResponse
|
||||
} = await request.post(
|
||||
INTEGRATION_GITLAB_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_id: CLIENT_ID_GITLAB,
|
||||
client_secret: CLIENT_SECRET_GITLAB,
|
||||
redirect_uri: `${SITE_URL}/integrations/gitlab/oauth2/callback`
|
||||
} as any),
|
||||
{
|
||||
headers: {
|
||||
"Accept-Encoding": "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + data.expires_in
|
||||
);
|
||||
|
||||
return ({
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
accessExpiresAt
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to refresh OAuth2 access token for GitLab');
|
||||
}
|
||||
};
|
||||
|
||||
export { exchangeRefresh };
|
||||
|
@@ -10,7 +10,8 @@ import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
} from '../variables';
|
||||
|
||||
const revokeAccess = async ({
|
||||
@@ -32,6 +33,8 @@ const revokeAccess = async ({
|
||||
break;
|
||||
case INTEGRATION_GITHUB:
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
break;
|
||||
}
|
||||
|
||||
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||
|
@@ -19,11 +19,13 @@ import {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_TRAVISCI,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_GITLAB_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
@@ -110,6 +112,13 @@ const syncSecrets = async ({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
await syncSecretsGitLab({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_RENDER:
|
||||
await syncSecretsRender({
|
||||
integration,
|
||||
@@ -163,7 +172,6 @@ const syncSecretsAzureKeyVault = async ({
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
|
||||
interface GetAzureKeyVaultSecret {
|
||||
id: string; // secret URI
|
||||
attributes: {
|
||||
@@ -186,17 +194,22 @@ const syncSecretsAzureKeyVault = async ({
|
||||
*/
|
||||
const paginateAzureKeyVaultSecrets = async (url: string) => {
|
||||
let result: GetAzureKeyVaultSecret[] = [];
|
||||
|
||||
while (url) {
|
||||
const res = await request.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
});
|
||||
try {
|
||||
while (url) {
|
||||
const res = await request.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
result = result.concat(res.data.value);
|
||||
|
||||
url = res.data.nextLink;
|
||||
}
|
||||
|
||||
result = result.concat(res.data.value);
|
||||
url = res.data.nextLink;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -212,8 +225,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
|
||||
const azureKeyVaultSecret = await request.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -259,33 +271,75 @@ const syncSecretsAzureKeyVault = async ({
|
||||
deleteSecrets.push(res[key]);
|
||||
}
|
||||
});
|
||||
|
||||
const setSecretAzureKeyVault = async ({
|
||||
key,
|
||||
value,
|
||||
integration,
|
||||
accessToken
|
||||
}: {
|
||||
key: string;
|
||||
value: string;
|
||||
integration: IIntegration;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let isSecretSet = false;
|
||||
let maxTries = 6;
|
||||
|
||||
while (!isSecretSet && maxTries > 0) {
|
||||
// try to set secret
|
||||
try {
|
||||
await request.put(
|
||||
`${integration.app}/secrets/${key}?api-version=7.3`,
|
||||
{
|
||||
value
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
isSecretSet = true;
|
||||
|
||||
} catch (err) {
|
||||
const error: any = err;
|
||||
if (error?.response?.data?.error?.innererror?.code === 'ObjectIsDeletedButRecoverable') {
|
||||
await request.post(
|
||||
`${integration.app}/deletedsecrets/${key}/recover?api-version=7.3`, {},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
} else {
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
maxTries--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sync/push set secrets
|
||||
if (setSecrets.length > 0) {
|
||||
setSecrets.forEach(async ({ key, value }) => {
|
||||
await request.put(
|
||||
`${integration.app}/secrets/${key}?api-version=7.3`,
|
||||
{
|
||||
value
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
for await (const setSecret of setSecrets) {
|
||||
const { key, value } = setSecret;
|
||||
setSecretAzureKeyVault({
|
||||
key,
|
||||
value,
|
||||
integration,
|
||||
accessToken
|
||||
});
|
||||
}
|
||||
|
||||
if (deleteSecrets.length > 0) {
|
||||
deleteSecrets.forEach(async (secret) => {
|
||||
await request.delete(`${integration.app}/secrets/${secret.key}?api-version=7.3`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
});
|
||||
for await (const deleteSecret of deleteSecrets) {
|
||||
const { key } = deleteSecret;
|
||||
await request.delete(`${integration.app}/secrets/${key}?api-version=7.3`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -1422,7 +1476,98 @@ const syncSecretsTravisCI = async ({
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to sync secrets to TravisCI");
|
||||
throw new Error("Failed to sync secrets to GitLab");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to GitLab repo with name [integration.app]
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
* @param {String} obj.accessToken - access token for GitLab integration
|
||||
*/
|
||||
const syncSecretsGitLab = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken,
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
secrets: any;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
// get secrets from gitlab
|
||||
const getSecretsRes = (
|
||||
await request.get(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`,
|
||||
{
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
).data;
|
||||
|
||||
for await (const key of Object.keys(secrets)) {
|
||||
const existingSecret = getSecretsRes.find((s: any) => s.key == key);
|
||||
if (!existingSecret) {
|
||||
await request.post(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`,
|
||||
{
|
||||
key: key,
|
||||
value: secrets[key],
|
||||
protected: false,
|
||||
masked: false,
|
||||
raw: false,
|
||||
environment_scope:'*'
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// udpate secret
|
||||
await request.put(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}`,
|
||||
{
|
||||
...existingSecret,
|
||||
value: secrets[existingSecret.key]
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"Accept-Encoding": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// delete secrets
|
||||
for await (const sec of getSecretsRes) {
|
||||
if (!(sec.key in secrets)) {
|
||||
await request.delete(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}`,
|
||||
{
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to sync secrets to GitLab");
|
||||
}
|
||||
}
|
||||
|
||||
|
92
backend/src/integrations/teams.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as Sentry from "@sentry/node";
|
||||
import {
|
||||
IIntegrationAuth
|
||||
} from '../models';
|
||||
import {
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_GITLAB_API_URL
|
||||
} from '../variables';
|
||||
import request from '../config/request';
|
||||
|
||||
interface Team {
|
||||
name: string;
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of teams for integration authorization [integrationAuth]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuth - integration authorization to get teams for
|
||||
* @param {String} obj.accessToken - access token for integration authorization
|
||||
* @returns {Object[]} teams - teams of integration authorization
|
||||
* @returns {String} teams.name - name of team
|
||||
* @returns {String} teams.teamId - id of team
|
||||
*/
|
||||
const getTeams = async ({
|
||||
integrationAuth,
|
||||
accessToken
|
||||
}: {
|
||||
integrationAuth: IIntegrationAuth;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
|
||||
let teams: Team[] = [];
|
||||
try {
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_GITLAB:
|
||||
teams = await getTeamsGitLab({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get integration teams');
|
||||
}
|
||||
|
||||
|
||||
return teams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of teams for GitLab integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - access token for GitLab API
|
||||
* @returns {Object[]} teams - teams that user is part of in GitLab
|
||||
* @returns {String} teams.name - name of team
|
||||
* @returns {String} teams.teamId - id of team
|
||||
*/
|
||||
const getTeamsGitLab = async ({
|
||||
accessToken
|
||||
}: {
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let teams: Team[] = [];
|
||||
try {
|
||||
const res = (await request.get(
|
||||
`${INTEGRATION_GITLAB_API_URL}/v4/groups`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
)).data;
|
||||
|
||||
teams = res.map((t: any) => ({
|
||||
name: t.name,
|
||||
teamId: t.id
|
||||
}));
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get GitLab integration teams");
|
||||
}
|
||||
|
||||
return teams;
|
||||
}
|
||||
|
||||
export {
|
||||
getTeams
|
||||
}
|
@@ -1,12 +1,12 @@
|
||||
import { ErrorRequestHandler } from "express";
|
||||
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { InternalServerError, UnauthorizedRequestError, UnprocessableEntityError } from "../utils/errors";
|
||||
import { InternalServerError, UnauthorizedRequestError } from "../utils/errors";
|
||||
import { getLogger } from "../utils/logger";
|
||||
import RequestError, { LogLevel } from "../utils/requestError";
|
||||
import { NODE_ENV } from "../config";
|
||||
|
||||
import mongoose from "mongoose";
|
||||
import { TokenExpiredError } from 'jsonwebtoken';
|
||||
|
||||
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => {
|
||||
if (res.headersSent) return next();
|
||||
@@ -34,17 +34,4 @@ export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | E
|
||||
|
||||
res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req))
|
||||
next()
|
||||
}
|
||||
|
||||
export const handleMongoInvalidDataError = (err: any, req: any, res: any, next: any) => {
|
||||
if (err instanceof mongoose.Error.ValidationError) {
|
||||
const errors: any = {};
|
||||
for (const field in err.errors) {
|
||||
errors[field] = err.errors[field].message;
|
||||
}
|
||||
|
||||
throw UnprocessableEntityError({ message: JSON.stringify(errors) })
|
||||
} else {
|
||||
next(err);
|
||||
}
|
||||
}
|
@@ -1,12 +1,14 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { User, ServiceTokenData } from '../models';
|
||||
import {
|
||||
validateAuthMode,
|
||||
getAuthUserPayload,
|
||||
getAuthSTDPayload,
|
||||
getAuthAPIKeyPayload
|
||||
} from '../helpers/auth';
|
||||
import {
|
||||
UnauthorizedRequestError
|
||||
} from '../utils/errors';
|
||||
|
||||
declare module 'jsonwebtoken' {
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
@@ -25,9 +27,11 @@ declare module 'jsonwebtoken' {
|
||||
* @returns
|
||||
*/
|
||||
const requireAuth = ({
|
||||
acceptedAuthModes = ['jwt']
|
||||
acceptedAuthModes = ['jwt'],
|
||||
requiredServiceTokenPermissions = []
|
||||
}: {
|
||||
acceptedAuthModes: string[];
|
||||
requiredServiceTokenPermissions?: string[];
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
// validate auth token against accepted auth modes [acceptedAuthModes]
|
||||
@@ -38,11 +42,22 @@ const requireAuth = ({
|
||||
});
|
||||
|
||||
// attach auth payloads
|
||||
let serviceTokenData: any;
|
||||
switch (authTokenType) {
|
||||
case 'serviceToken':
|
||||
req.serviceTokenData = await getAuthSTDPayload({
|
||||
serviceTokenData = await getAuthSTDPayload({
|
||||
authTokenValue
|
||||
});
|
||||
|
||||
requiredServiceTokenPermissions.forEach((requiredServiceTokenPermission) => {
|
||||
if (!serviceTokenData.permissions.includes(requiredServiceTokenPermission)) {
|
||||
return next(UnauthorizedRequestError({ message: 'Failed to authorize service token for endpoint' }));
|
||||
}
|
||||
});
|
||||
|
||||
req.serviceTokenData = serviceTokenData;
|
||||
req.user = serviceTokenData?.user;
|
||||
|
||||
break;
|
||||
case 'apiKey':
|
||||
req.user = await getAuthAPIKeyPayload({
|
||||
|
@@ -23,7 +23,7 @@ const requireSecretsAuth = ({
|
||||
// case: validate 1 secret
|
||||
secrets = await validateSecrets({
|
||||
userId: req.user._id.toString(),
|
||||
secretIds: req.body.secrets.id
|
||||
secretIds: [req.body.secrets.id]
|
||||
});
|
||||
} else if (Array.isArray(req.body.secretIds)) {
|
||||
secrets = await validateSecrets({
|
||||
|
@@ -21,7 +21,7 @@ const requireWorkspaceAuth = ({
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { workspaceId } = req[location];
|
||||
|
||||
|
||||
if (req.user) {
|
||||
// case: jwt auth
|
||||
const membership = await validateMembership({
|
||||
@@ -32,11 +32,11 @@ const requireWorkspaceAuth = ({
|
||||
|
||||
req.membership = membership;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
req.serviceTokenData
|
||||
&& req.serviceTokenData.workspace !== workspaceId
|
||||
&& req.serviceTokenData.environment !== req.query.environment
|
||||
&& req.serviceTokenData.workspace.toString() !== workspaceId
|
||||
&& req.serviceTokenData.environment !== req.body.environment
|
||||
) {
|
||||
next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
@@ -31,7 +32,8 @@ export interface IIntegration {
|
||||
| 'heroku'
|
||||
| 'vercel'
|
||||
| 'netlify'
|
||||
| 'github'
|
||||
| 'github'
|
||||
| 'gitlab'
|
||||
| 'render'
|
||||
| 'flyio'
|
||||
| 'circleci'
|
||||
@@ -60,13 +62,11 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
default: null,
|
||||
},
|
||||
appId: {
|
||||
// (new)
|
||||
// id of app in provider
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
targetEnvironment: {
|
||||
// (new)
|
||||
// target environment
|
||||
type: String,
|
||||
default: null,
|
||||
@@ -96,6 +96,7 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
@@ -16,15 +17,15 @@ import {
|
||||
export interface IIntegrationAuth {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'aws-parameter-store' | 'aws-secret-manager';
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'aws-parameter-store' | 'aws-secret-manager';
|
||||
teamId: string;
|
||||
accountId: string;
|
||||
refreshCiphertext?: string;
|
||||
refreshIV?: string;
|
||||
refreshTag?: string;
|
||||
accessIdCiphertext?: string; // new
|
||||
accessIdIV?: string; // new
|
||||
accessIdTag?: string; // new
|
||||
accessIdCiphertext?: string;
|
||||
accessIdIV?: string;
|
||||
accessIdTag?: string;
|
||||
accessCiphertext?: string;
|
||||
accessIV?: string;
|
||||
accessTag?: string;
|
||||
@@ -48,6 +49,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
|
@@ -26,7 +26,7 @@ export interface ISecret {
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export const secretSchema = new Schema<ISecret>(
|
||||
const secretSchema = new Schema<ISecret>(
|
||||
{
|
||||
version: {
|
||||
type: Number,
|
||||
|
@@ -1,28 +1,18 @@
|
||||
import mongoose, { Schema, model } from 'mongoose';
|
||||
import Secret, { ISecret, secretSchema } from './secret';
|
||||
|
||||
export interface IRequestedChange {
|
||||
_id: string
|
||||
userId: mongoose.Types.ObjectId;
|
||||
status: ApprovalStatus;
|
||||
modifiedSecretDetails: ISecret,
|
||||
modifiedSecretParentId: mongoose.Types.ObjectId,
|
||||
type: string,
|
||||
approvers: IApprover[]
|
||||
merged: boolean
|
||||
}
|
||||
import Secret, { ISecret } from './secret';
|
||||
|
||||
interface ISecretApprovalRequest {
|
||||
environment: string;
|
||||
workspace: mongoose.Types.ObjectId;
|
||||
requestedChanges: IRequestedChange[];
|
||||
requestedByUserId: mongoose.Types.ObjectId;
|
||||
secret: mongoose.Types.ObjectId;
|
||||
requestedChanges: ISecret;
|
||||
requestedBy: mongoose.Types.ObjectId;
|
||||
approvers: IApprover[];
|
||||
status: ApprovalStatus;
|
||||
timestamp: Date;
|
||||
requestType: ChangeType;
|
||||
requestType: RequestType;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export interface IApprover {
|
||||
interface IApprover {
|
||||
userId: mongoose.Types.ObjectId;
|
||||
status: ApprovalStatus;
|
||||
}
|
||||
@@ -33,80 +23,54 @@ export enum ApprovalStatus {
|
||||
REJECTED = 'rejected'
|
||||
}
|
||||
|
||||
export enum ChangeType {
|
||||
export enum RequestType {
|
||||
UPDATE = 'update',
|
||||
DELETE = 'delete',
|
||||
CREATE = 'create'
|
||||
}
|
||||
|
||||
const approverSchema = new mongoose.Schema({
|
||||
userId: {
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: false,
|
||||
required: true
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: [ApprovalStatus],
|
||||
default: ApprovalStatus.PENDING
|
||||
}
|
||||
}, { timestamps: true });
|
||||
|
||||
|
||||
// extend the Secret Schema by taking all but removing _id and version fields
|
||||
const SecretModificationSchema = new Schema({
|
||||
...secretSchema.obj,
|
||||
}, {
|
||||
_id: false,
|
||||
});
|
||||
|
||||
SecretModificationSchema.remove("version")
|
||||
|
||||
|
||||
const requestedChangeSchema = new mongoose.Schema(
|
||||
{
|
||||
_id: { type: mongoose.Schema.Types.ObjectId, auto: true },
|
||||
modifiedSecretDetails: SecretModificationSchema,
|
||||
modifiedSecretParentId: { // used to fetch the current version of this secret for comparing
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Secret'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ChangeType,
|
||||
required: true
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ApprovalStatus,
|
||||
default: ApprovalStatus.PENDING // the overall status of the requested change
|
||||
},
|
||||
approvers: [approverSchema],
|
||||
merged: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
|
||||
{
|
||||
environment: {
|
||||
type: String, // The secret changes were requested for
|
||||
secret: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Secret'
|
||||
},
|
||||
workspace: {
|
||||
type: mongoose.Schema.Types.ObjectId, // workspace id of the secret
|
||||
ref: 'Workspace'
|
||||
},
|
||||
requestedChanges: [requestedChangeSchema], // the changes that the requested user wants to make to the existing secret
|
||||
requestedByUserId: {
|
||||
requestedChanges: Secret,
|
||||
requestedBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
approvers: [approverSchema],
|
||||
status: {
|
||||
type: String,
|
||||
enum: ApprovalStatus,
|
||||
default: ApprovalStatus.PENDING
|
||||
},
|
||||
timestamp: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
requestType: {
|
||||
type: String,
|
||||
enum: RequestType,
|
||||
required: true
|
||||
},
|
||||
requestId: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -114,8 +78,6 @@ const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
|
||||
}
|
||||
);
|
||||
|
||||
secretApprovalRequestSchema.index({ 'requestedChanges.approvers.userId': 1 });
|
||||
|
||||
const SecretApprovalRequest = model<ISecretApprovalRequest>('secret_approval_request', secretApprovalRequestSchema);
|
||||
const SecretApprovalRequest = model<ISecretApprovalRequest>('SecretApprovalRequest', secretApprovalRequestSchema);
|
||||
|
||||
export default SecretApprovalRequest;
|
||||
|
@@ -3,13 +3,14 @@ import { Schema, model, Types } from 'mongoose';
|
||||
export interface IServiceTokenData {
|
||||
name: string;
|
||||
workspace: Types.ObjectId;
|
||||
environment: string; // TODO: adapt to upcoming environment id
|
||||
environment: string;
|
||||
user: Types.ObjectId;
|
||||
expiresAt: Date;
|
||||
secretHash: string;
|
||||
encryptedKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
const serviceTokenDataSchema = new Schema<IServiceTokenData>(
|
||||
@@ -51,6 +52,11 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
|
||||
tag: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
permissions: {
|
||||
type: [String],
|
||||
enum: ['read', 'write'],
|
||||
default: ['read']
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@@ -1,16 +1,9 @@
|
||||
import mongoose, { Schema, model, Types } from 'mongoose';
|
||||
|
||||
|
||||
export interface DesignatedApprovers {
|
||||
environment: string,
|
||||
approvers: [mongoose.Schema.Types.ObjectId]
|
||||
}
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
|
||||
export interface IWorkspace {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
organization: Types.ObjectId;
|
||||
approvers: [DesignatedApprovers];
|
||||
environments: Array<{
|
||||
name: string;
|
||||
slug: string;
|
||||
@@ -18,16 +11,6 @@ export interface IWorkspace {
|
||||
autoCapitalization: boolean;
|
||||
}
|
||||
|
||||
const approverSchema = new mongoose.Schema({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
environment: {
|
||||
type: String
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
const workspaceSchema = new Schema<IWorkspace>({
|
||||
name: {
|
||||
type: String,
|
||||
@@ -37,7 +20,6 @@ const workspaceSchema = new Schema<IWorkspace>({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
approvers: [approverSchema],
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Organization',
|
||||
|
@@ -15,7 +15,6 @@ import password from './password';
|
||||
import stripe from './stripe';
|
||||
import integration from './integration';
|
||||
import integrationAuth from './integrationAuth';
|
||||
import secretApprovalRequest from './secretApprovalsRequest'
|
||||
|
||||
export {
|
||||
signup,
|
||||
@@ -34,6 +33,5 @@ export {
|
||||
password,
|
||||
stripe,
|
||||
integration,
|
||||
integrationAuth,
|
||||
secretApprovalRequest
|
||||
integrationAuth
|
||||
};
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body, param } from 'express-validator';
|
||||
import { body, param, query } from 'express-validator';
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
@@ -73,10 +73,24 @@ router.get(
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('integrationAuthId'),
|
||||
query('teamId'),
|
||||
validateRequest,
|
||||
integrationAuthController.getIntegrationAuthApps
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:integrationAuthId/teams',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('integrationAuthId'),
|
||||
validateRequest,
|
||||
integrationAuthController.getIntegrationAuthTeams
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:integrationAuthId',
|
||||
requireAuth({
|
||||
|
@@ -1,65 +0,0 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { secretApprovalController } from '../../controllers/v1';
|
||||
import { body, param } from 'express-validator';
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('workspaceId').exists(),
|
||||
body('environment').exists(),
|
||||
body('requestedChanges').isArray(),
|
||||
validateRequest,
|
||||
secretApprovalController.createApprovalRequest
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/sent',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
secretApprovalController.getAllApprovalRequestsForUser
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/approvals-needed',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
secretApprovalController.getAllApprovalRequestsThatRequireUserApproval
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:reviewId/approve',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('requestedChangeIds').isArray(),
|
||||
validateRequest,
|
||||
secretApprovalController.approveApprovalRequest
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:reviewId/reject',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('requestedChangeIds').isArray(),
|
||||
validateRequest,
|
||||
secretApprovalController.rejectApprovalRequest
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:reviewId/merge',
|
||||
body('requestedChangeIds').isArray(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
secretApprovalController.mergeApprovalRequestSecrets
|
||||
);
|
||||
|
||||
export default router;
|
@@ -36,10 +36,10 @@ router.get(
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
}),
|
||||
workspaceController.getWorkspaces
|
||||
);
|
||||
|
||||
@@ -134,34 +134,6 @@ router.get(
|
||||
workspaceController.getWorkspaceIntegrationAuthorizations
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:workspaceId/approvers',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body("approvers").isArray(),
|
||||
validateRequest,
|
||||
workspaceController.addApproverForWorkspaceAndEnvironment
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:workspaceId/approvers',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body("approvers").isArray(),
|
||||
validateRequest,
|
||||
workspaceController.removeApproverForWorkspaceAndEnvironment
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/service-tokens', // deprecate
|
||||
requireAuth({
|
||||
|
@@ -22,7 +22,8 @@ import {
|
||||
router.post(
|
||||
'/batch',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['read', 'write']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -99,7 +100,8 @@ router.post(
|
||||
}),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['write']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -115,7 +117,8 @@ router.get(
|
||||
query('tagSlugs'),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['read']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -154,7 +157,8 @@ router.patch(
|
||||
}),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['write']
|
||||
}),
|
||||
requireSecretsAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@@ -182,7 +186,8 @@ router.delete(
|
||||
.isEmpty(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['write']
|
||||
}),
|
||||
requireSecretsAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@@ -192,5 +197,3 @@ router.delete(
|
||||
|
||||
export default router;
|
||||
|
||||
|
||||
|
||||
|
@@ -30,13 +30,22 @@ router.post(
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
}),
|
||||
body('name').exists().trim(),
|
||||
body('workspaceId'),
|
||||
body('environment'),
|
||||
body('encryptedKey'),
|
||||
body('iv'),
|
||||
body('tag'),
|
||||
body('expiresIn'), // measured in ms
|
||||
body('name').exists().isString().trim(),
|
||||
body('workspaceId').exists().isString().trim(),
|
||||
body('environment').exists().isString().trim(),
|
||||
body('encryptedKey').exists().isString().trim(),
|
||||
body('iv').exists().isString().trim(),
|
||||
body('tag').exists().isString().trim(),
|
||||
body('expiresIn').exists().isNumeric(), // measured in ms
|
||||
body('permissions').isArray({ min: 1 }).custom((value: string[]) => {
|
||||
const allowedPermissions = ['read', 'write'];
|
||||
const invalidValues = value.filter((v) => !allowedPermissions.includes(v));
|
||||
if (invalidValues.length > 0) {
|
||||
throw new Error(`permissions contains invalid values: ${invalidValues.join(', ')}`);
|
||||
}
|
||||
|
||||
return true
|
||||
}),
|
||||
validateRequest,
|
||||
serviceTokenDataController.createServiceTokenData
|
||||
);
|
||||
|
@@ -7,9 +7,6 @@ import {
|
||||
setIntegrationAuthAccessHelper,
|
||||
} from '../helpers/integration';
|
||||
|
||||
// should sync stuff be here too? Probably.
|
||||
// TODO: move bot functions to IntegrationService.
|
||||
|
||||
/**
|
||||
* Class to handle integrations
|
||||
*/
|
||||
|
@@ -1,9 +1,16 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_SECURE } from '../config';
|
||||
import {
|
||||
SMTP_HOST,
|
||||
SMTP_PORT,
|
||||
SMTP_USERNAME,
|
||||
SMTP_PASSWORD,
|
||||
SMTP_SECURE
|
||||
} from '../config';
|
||||
import {
|
||||
SMTP_HOST_SENDGRID,
|
||||
SMTP_HOST_MAILGUN,
|
||||
SMTP_HOST_SOCKETLABS
|
||||
SMTP_HOST_SOCKETLABS,
|
||||
SMTP_HOST_ZOHOMAIL
|
||||
} from '../variables';
|
||||
import SMTPConnection from 'nodemailer/lib/smtp-connection';
|
||||
import * as Sentry from '@sentry/node';
|
||||
@@ -37,6 +44,12 @@ if (SMTP_SECURE) {
|
||||
ciphers: 'TLSv1.2'
|
||||
}
|
||||
break;
|
||||
case SMTP_HOST_ZOHOMAIL:
|
||||
mailOpts.requireTLS = true;
|
||||
mailOpts.tls = {
|
||||
ciphers: 'TLSv1.2'
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (SMTP_HOST.includes('amazonaws.com')) {
|
||||
mailOpts.tls = {
|
||||
|
@@ -19,15 +19,6 @@ export const MethodNotAllowedError = (error?: Partial<RequestErrorContext>) => n
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
export const UnprocessableEntityError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||
statusCode: error?.statusCode ?? 422,
|
||||
type: error?.type ?? 'unprocessable_entity',
|
||||
message: error?.message ?? 'The server understands the content of the request, but it was unable to process it because it contains invalid data',
|
||||
context: error?.context,
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
export const UnauthorizedRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||
statusCode: error?.statusCode ?? 401,
|
||||
@@ -36,7 +27,7 @@ export const UnauthorizedRequestError = (error?: Partial<RequestErrorContext>) =
|
||||
context: error?.context,
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
|
||||
export const ForbiddenRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||
statusCode: error?.statusCode ?? 403,
|
||||
@@ -55,15 +46,6 @@ export const BadRequestError = (error?: Partial<RequestErrorContext>) => new Req
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
export const ResourceNotFound = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||
statusCode: error?.statusCode ?? 404,
|
||||
type: error?.type ?? 'resource_not_found',
|
||||
message: error?.message ?? 'The requested resource was not found',
|
||||
context: error?.context,
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
export const InternalServerError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||
statusCode: error?.statusCode ?? 500,
|
||||
|
@@ -13,6 +13,7 @@ import {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
@@ -24,7 +25,9 @@ import {
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
INTEGRATION_GITHUB_TOKEN_URL,
|
||||
INTEGRATION_GITLAB_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_GITLAB_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
@@ -47,7 +50,8 @@ import {
|
||||
import {
|
||||
SMTP_HOST_SENDGRID,
|
||||
SMTP_HOST_MAILGUN,
|
||||
SMTP_HOST_SOCKETLABS
|
||||
SMTP_HOST_SOCKETLABS,
|
||||
SMTP_HOST_ZOHOMAIL
|
||||
} from './smtp';
|
||||
import { PLAN_STARTER, PLAN_PRO } from './stripe';
|
||||
import {
|
||||
@@ -80,6 +84,7 @@ export {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
@@ -91,7 +96,9 @@ export {
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
INTEGRATION_GITHUB_TOKEN_URL,
|
||||
INTEGRATION_GITLAB_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_GITLAB_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
@@ -110,6 +117,7 @@ export {
|
||||
SMTP_HOST_SENDGRID,
|
||||
SMTP_HOST_MAILGUN,
|
||||
SMTP_HOST_SOCKETLABS,
|
||||
SMTP_HOST_ZOHOMAIL,
|
||||
PLAN_STARTER,
|
||||
PLAN_PRO,
|
||||
MFA_METHOD_EMAIL,
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
CLIENT_ID_AZURE,
|
||||
TENANT_ID_AZURE
|
||||
CLIENT_ID_GITLAB
|
||||
} from '../config';
|
||||
import {
|
||||
CLIENT_ID_HEROKU,
|
||||
CLIENT_ID_NETLIFY,
|
||||
CLIENT_ID_GITHUB,
|
||||
CLIENT_SLUG_VERCEL,
|
||||
CLIENT_SLUG_VERCEL
|
||||
} from "../config";
|
||||
|
||||
// integrations
|
||||
@@ -17,6 +17,7 @@ const INTEGRATION_HEROKU = "heroku";
|
||||
const INTEGRATION_VERCEL = "vercel";
|
||||
const INTEGRATION_NETLIFY = "netlify";
|
||||
const INTEGRATION_GITHUB = "github";
|
||||
const INTEGRATION_GITLAB = "gitlab";
|
||||
const INTEGRATION_RENDER = "render";
|
||||
const INTEGRATION_FLYIO = "flyio";
|
||||
const INTEGRATION_CIRCLECI = "circleci";
|
||||
@@ -27,6 +28,7 @@ const INTEGRATION_SET = new Set([
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
@@ -37,16 +39,19 @@ const INTEGRATION_SET = new Set([
|
||||
const INTEGRATION_OAUTH2 = "oauth2";
|
||||
|
||||
// integration oauth endpoints
|
||||
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
|
||||
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/common/oauth2/v2.0/token`;
|
||||
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
||||
const INTEGRATION_VERCEL_TOKEN_URL =
|
||||
"https://api.vercel.com/v2/oauth/access_token";
|
||||
const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
|
||||
const INTEGRATION_GITHUB_TOKEN_URL =
|
||||
"https://github.com/login/oauth/access_token";
|
||||
const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token";
|
||||
|
||||
|
||||
// integration apps endpoints
|
||||
const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
|
||||
const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api";
|
||||
const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
|
||||
const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
|
||||
const INTEGRATION_RENDER_API_URL = "https://api.render.com";
|
||||
@@ -54,6 +59,7 @@ const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
|
||||
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
|
||||
const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
|
||||
|
||||
// TODO: deprecate types?
|
||||
const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Heroku',
|
||||
@@ -128,6 +134,15 @@ const INTEGRATION_OPTIONS = [
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Azure Key Vault',
|
||||
slug: 'azure-key-vault',
|
||||
image: 'Microsoft Azure.png',
|
||||
isAvailable: true,
|
||||
type: 'oauth',
|
||||
clientId: CLIENT_ID_AZURE,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Circle CI',
|
||||
slug: 'circleci',
|
||||
@@ -137,6 +152,15 @@ const INTEGRATION_OPTIONS = [
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'GitLab',
|
||||
slug: 'gitlab',
|
||||
image: 'GitLab.png',
|
||||
isAvailable: true,
|
||||
type: 'custom',
|
||||
clientId: CLIENT_ID_GITLAB,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Travis CI',
|
||||
slug: 'travisci',
|
||||
@@ -146,16 +170,6 @@ const INTEGRATION_OPTIONS = [
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Azure Key Vault',
|
||||
slug: 'azure-key-vault',
|
||||
image: 'Microsoft Azure.png',
|
||||
isAvailable: false,
|
||||
type: 'oauth',
|
||||
clientId: CLIENT_ID_AZURE,
|
||||
tenantId: TENANT_ID_AZURE,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Google Cloud Platform',
|
||||
slug: 'gcp',
|
||||
@@ -175,6 +189,7 @@ export {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
@@ -186,7 +201,9 @@ export {
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
INTEGRATION_GITHUB_TOKEN_URL,
|
||||
INTEGRATION_GITLAB_API_URL,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_GITLAB_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
|
@@ -1,9 +1,11 @@
|
||||
const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net';
|
||||
const SMTP_HOST_MAILGUN = 'smtp.mailgun.org';
|
||||
const SMTP_HOST_SOCKETLABS = 'smtp.socketlabs.com';
|
||||
const SMTP_HOST_ZOHOMAIL = 'smtp.zoho.com';
|
||||
|
||||
export {
|
||||
SMTP_HOST_SENDGRID,
|
||||
SMTP_HOST_MAILGUN,
|
||||
SMTP_HOST_SOCKETLABS
|
||||
SMTP_HOST_SOCKETLABS,
|
||||
SMTP_HOST_ZOHOMAIL
|
||||
}
|
@@ -197,6 +197,23 @@ const generateOpenAPISpec = async () => {
|
||||
secretValueCiphertext: '',
|
||||
secretValueIV: '',
|
||||
secretValueTag: '',
|
||||
},
|
||||
ServiceTokenData: {
|
||||
_id: '',
|
||||
name: '',
|
||||
workspace: '',
|
||||
environment: '',
|
||||
user: {
|
||||
_id: '',
|
||||
firstName: '',
|
||||
lastName: ''
|
||||
},
|
||||
expiresAt: '2023-01-13T14:16:12.210Z',
|
||||
encryptedKey: '',
|
||||
iv: '',
|
||||
tag: '',
|
||||
updatedAt: '2023-01-13T14:16:12.210Z',
|
||||
createdAt: '2023-01-13T14:16:12.210Z'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -7,7 +7,7 @@ require (
|
||||
github.com/muesli/mango-cobra v1.2.0
|
||||
github.com/muesli/roff v0.1.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
golang.org/x/crypto v0.6.0
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
||||
golang.org/x/term v0.5.0
|
||||
)
|
||||
|
||||
@@ -31,7 +31,7 @@ require (
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.10.0 // indirect
|
||||
golang.org/x/net v0.6.0 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
)
|
||||
|
||||
|
16
cli/go.sum
@@ -103,17 +103,13 @@ github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgk
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg=
|
||||
go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -125,13 +121,9 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
@@ -49,6 +49,11 @@ var exportCmd = &cobra.Command{
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
projectId, err := cmd.Flags().GetString("projectId")
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
format, err := cmd.Flags().GetString("format")
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
@@ -69,7 +74,7 @@ var exportCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, WorkspaceId: projectId})
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to fetch secrets")
|
||||
}
|
||||
@@ -106,6 +111,7 @@ func init() {
|
||||
exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||
exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||
exportCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs")
|
||||
exportCmd.Flags().String("projectId", "", "manually set the projectId to fetch secrets from")
|
||||
}
|
||||
|
||||
// Format according to the format flag
|
||||
|
@@ -6,7 +6,6 @@ package cmd
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
@@ -96,7 +95,7 @@ func writeWorkspaceFile(selectedWorkspace models.Workspace) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = util.WriteToFile(util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, marshalledWorkspaceFile, os.ModePerm)
|
||||
err = util.WriteToFile(util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, marshalledWorkspaceFile, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@@ -41,13 +41,14 @@ var loginCmd = &cobra.Command{
|
||||
PreRun: toggleDebug,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
||||
if err != nil && (strings.Contains(err.Error(), "The specified item could not be found in the keyring") || strings.Contains(err.Error(), "unable to get key from Keyring")) { // if the key can't be found allow them to override
|
||||
// if the key can't be found or there is an error getting current credentials from key ring, allow them to override
|
||||
if err != nil && (strings.Contains(err.Error(), "The specified item could not be found in the keyring") || strings.Contains(err.Error(), "unable to get key from Keyring") || strings.Contains(err.Error(), "GetUserCredsFromKeyRing")) {
|
||||
log.Debug(err)
|
||||
} else if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired { // if you are logged in but not expired
|
||||
if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired && len(currentLoggedInUserDetails.UserCredentials.PrivateKey) != 0 {
|
||||
shouldOverride, err := shouldOverrideLoginPrompt(currentLoggedInUserDetails.UserCredentials.Email)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
@@ -234,8 +235,16 @@ var loginCmd = &cobra.Command{
|
||||
// clear backed up secrets from prev account
|
||||
util.DeleteBackupSecrets()
|
||||
|
||||
color.Green("Nice! You are logged in as: %v", email)
|
||||
whilte := color.New(color.FgGreen)
|
||||
boldWhite := whilte.Add(color.Bold)
|
||||
boldWhite.Printf(">>>> Welcome to Infisical!")
|
||||
boldWhite.Printf(" You are now logged in as %v <<<< \n", email)
|
||||
|
||||
plainBold := color.New(color.Bold)
|
||||
|
||||
plainBold.Println("\nQuick links")
|
||||
fmt.Println("- Learn to inject secrets into your application at https://infisical.com/docs/cli/usage")
|
||||
fmt.Println("- Stuck? Join our slack for quick support https://infisical.com/slack")
|
||||
},
|
||||
}
|
||||
|
||||
|
@@ -36,6 +36,9 @@ var resetCmd = &cobra.Command{
|
||||
|
||||
keyringInstance.Remove(util.KEYRING_SERVICE_NAME)
|
||||
|
||||
// delete secrets backup
|
||||
util.DeleteBackupSecrets()
|
||||
|
||||
util.PrintSuccessMessage("Reset successful")
|
||||
},
|
||||
}
|
||||
|
@@ -186,14 +186,11 @@ func init() {
|
||||
|
||||
// Will execute a single command and pass in the given secrets into the process
|
||||
func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string) error {
|
||||
shell := subShellCmd()
|
||||
command := args[0]
|
||||
argsForCommand := args[1:]
|
||||
color.Green("Injecting %v Infisical secrets into your application process", secretsCount)
|
||||
|
||||
args = append(args[:1], args[0:]...) // shift args to the right
|
||||
args[0] = shell[1]
|
||||
|
||||
cmd := exec.Command(shell[0], args...)
|
||||
|
||||
cmd := exec.Command(command, argsForCommand...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -203,7 +200,15 @@ func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string)
|
||||
}
|
||||
|
||||
func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []string) error {
|
||||
shell := subShellCmd()
|
||||
shell := [2]string{"sh", "-c"}
|
||||
if runtime.GOOS == "windows" {
|
||||
shell = [2]string{"cmd", "/C"}
|
||||
} else {
|
||||
currentShell := os.Getenv("SHELL")
|
||||
if currentShell != "" {
|
||||
shell[0] = currentShell
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command(shell[0], shell[1], fullCommand)
|
||||
cmd.Stdin = os.Stdin
|
||||
@@ -217,23 +222,6 @@ func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []
|
||||
return execCmd(cmd)
|
||||
}
|
||||
|
||||
func subShellCmd() [2]string {
|
||||
// default to sh -c
|
||||
shell := [...]string{"sh", "-c"}
|
||||
|
||||
currentShell := os.Getenv("SHELL")
|
||||
if currentShell != "" {
|
||||
shell[0] = currentShell
|
||||
} else if runtime.GOOS == "windows" {
|
||||
// if the SHELL env var is not set and we're on Windows, use cmd.exe
|
||||
// The SHELL var should always be checked first, in case the user executes
|
||||
// infisical from something like Git Bash.
|
||||
return [...]string{"cmd", "/C"}
|
||||
}
|
||||
|
||||
return shell
|
||||
}
|
||||
|
||||
// Credit: inspired by AWS Valut
|
||||
func execCmd(cmd *exec.Cmd) error {
|
||||
sigChannel := make(chan os.Signal, 1)
|
||||
|
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/Infisical/infisical-merge/packages/visualize"
|
||||
"github.com/go-resty/resty/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -132,6 +133,11 @@ var secretsSetCmd = &cobra.Command{
|
||||
encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce)
|
||||
currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey)
|
||||
|
||||
if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 {
|
||||
log.Debugf("Missing credentials for generating plainTextEncryptionKey: [currentUsersPrivateKey=%s] [encryptedWorkspaceKeySenderPublicKey=%s]", currentUsersPrivateKey, encryptedWorkspaceKeySenderPublicKey)
|
||||
util.PrintErrorMessageAndExit("Some required user credentials are missing to generate your [plainTextEncryptionKey]. Please run [infisical login] then try again")
|
||||
}
|
||||
|
||||
// decrypt workspace key
|
||||
plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
|
||||
|
||||
|
@@ -55,4 +55,5 @@ type GetAllSecretsParameters struct {
|
||||
EnvironmentPassedViaFlag bool
|
||||
InfisicalToken string
|
||||
TagSlugs string
|
||||
WorkspaceId string
|
||||
}
|
||||
|
@@ -4,18 +4,47 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
func CheckForUpdate() {
|
||||
if checkEnv := os.Getenv("INFISICAL_DISABLE_UPDATE_CHECK"); checkEnv != "" {
|
||||
return
|
||||
}
|
||||
latestVersion, err := getLatestTag("Infisical", "infisical")
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
// do nothing and continue
|
||||
return
|
||||
}
|
||||
if latestVersion != CLI_VERSION {
|
||||
PrintWarning(fmt.Sprintf("Please update your CLI. You are running version %s but the latest version is %s", CLI_VERSION, latestVersion))
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
blue := color.New(color.FgCyan).SprintFunc()
|
||||
black := color.New(color.FgBlack).SprintFunc()
|
||||
|
||||
msg := fmt.Sprintf("%s %s %s %s",
|
||||
yellow("A new release of infisical is available:"),
|
||||
blue(CLI_VERSION),
|
||||
black("->"),
|
||||
blue(latestVersion),
|
||||
)
|
||||
|
||||
fmt.Fprintln(os.Stderr, msg)
|
||||
|
||||
updateInstructions := GetUpdateInstructions()
|
||||
|
||||
if updateInstructions != "" {
|
||||
msg = fmt.Sprintf("\n%s\n", GetUpdateInstructions())
|
||||
fmt.Fprintln(os.Stderr, msg)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,12 +55,12 @@ func getLatestTag(repoOwner string, repoName string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return "", errors.New(fmt.Sprintf("GitHub API returned status code %d", resp.StatusCode))
|
||||
return "", errors.New(fmt.Sprintf("gitHub API returned status code %d", resp.StatusCode))
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -40,7 +69,59 @@ func getLatestTag(repoOwner string, repoName string) (string, error) {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
json.Unmarshal(body, &tags)
|
||||
if err := json.Unmarshal(body, &tags); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal github response: %w", err)
|
||||
}
|
||||
|
||||
return tags[0].Name[1:], nil
|
||||
}
|
||||
|
||||
func GetUpdateInstructions() string {
|
||||
os := runtime.GOOS
|
||||
switch os {
|
||||
case "darwin":
|
||||
return "To update, run: brew update && brew upgrade infisical"
|
||||
case "windows":
|
||||
return "To update, run: scoop update infisical"
|
||||
case "linux":
|
||||
pkgManager := getLinuxPackageManager()
|
||||
switch pkgManager {
|
||||
case "apt-get":
|
||||
return "To update, run: sudo apt-get update && sudo apt-get install infisical"
|
||||
case "yum":
|
||||
return "To update, run: sudo yum update infisical"
|
||||
case "apk":
|
||||
return "To update, run: sudo apk update && sudo apk upgrade infisical"
|
||||
case "yay":
|
||||
return "To update, run: yay -Syu infisical"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func getLinuxPackageManager() string {
|
||||
cmd := exec.Command("apt-get", "--version")
|
||||
if err := cmd.Run(); err == nil {
|
||||
return "apt-get"
|
||||
}
|
||||
|
||||
cmd = exec.Command("yum", "--version")
|
||||
if err := cmd.Run(); err == nil {
|
||||
return "yum"
|
||||
}
|
||||
|
||||
cmd = exec.Command("yay", "--version")
|
||||
if err := cmd.Run(); err == nil {
|
||||
return "yay"
|
||||
}
|
||||
|
||||
cmd = exec.Command("apk", "--version")
|
||||
if err := cmd.Run(); err == nil {
|
||||
return "apk"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
@@ -42,7 +42,7 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
|
||||
}
|
||||
|
||||
// Create file in directory
|
||||
err = WriteToFile(fullConfigFilePath, configFileMarshalled, os.ModePerm)
|
||||
err = WriteToFile(fullConfigFilePath, configFileMarshalled, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -151,52 +151,6 @@ func GetWorkspaceConfigByPath(path string) (workspaceConfig models.WorkspaceConf
|
||||
return workspaceConfigFile, nil
|
||||
}
|
||||
|
||||
// Will get the list of .infisical.json files that are located
|
||||
// within the root of each sub folder from where the CLI is ran from
|
||||
func GetAllWorkSpaceConfigsStartingFromCurrentPath() (workspaces []models.WorkspaceConfigFile, err error) {
|
||||
currentDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAllProjectConfigs: unable to get the current directory because [%s]", err)
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(currentDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAllProjectConfigs: unable to read the contents of the current directory because [%s]", err)
|
||||
}
|
||||
|
||||
listOfWorkSpaceConfigs := []models.WorkspaceConfigFile{}
|
||||
for _, file := range files {
|
||||
if !file.IsDir() && file.Name() == INFISICAL_WORKSPACE_CONFIG_FILE_NAME {
|
||||
pathToWorkspaceConfigFile := currentDir + "/" + INFISICAL_WORKSPACE_CONFIG_FILE_NAME
|
||||
|
||||
workspaceConfig, err := GetWorkspaceConfigByPath(pathToWorkspaceConfigFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAllProjectConfigs: Unable to get config file because [%s]", err)
|
||||
}
|
||||
|
||||
listOfWorkSpaceConfigs = append(listOfWorkSpaceConfigs, workspaceConfig)
|
||||
|
||||
} else if file.IsDir() {
|
||||
pathToSubFolder := currentDir + "/" + file.Name()
|
||||
pathToMaybeWorkspaceConfigFile := pathToSubFolder + "/" + INFISICAL_WORKSPACE_CONFIG_FILE_NAME
|
||||
|
||||
_, err := os.Stat(pathToMaybeWorkspaceConfigFile)
|
||||
if err != nil {
|
||||
continue // workspace config file doesn't exist
|
||||
}
|
||||
|
||||
workspaceConfig, err := GetWorkspaceConfigByPath(pathToMaybeWorkspaceConfigFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAllProjectConfigs: Unable to get config file because [%s]", err)
|
||||
}
|
||||
|
||||
listOfWorkSpaceConfigs = append(listOfWorkSpaceConfigs, workspaceConfig)
|
||||
}
|
||||
}
|
||||
|
||||
return listOfWorkSpaceConfigs, nil
|
||||
}
|
||||
|
||||
// Get the infisical config file and if it doesn't exist, return empty config model, otherwise raise error
|
||||
func GetConfigFile() (models.ConfigFile, error) {
|
||||
fullConfigFilePath, _, err := GetFullConfigFilePath()
|
||||
@@ -243,7 +197,7 @@ func WriteConfigFile(configFile *models.ConfigFile) error {
|
||||
}
|
||||
|
||||
// Create file in directory
|
||||
err = os.WriteFile(fullConfigFilePath, configFileMarshalled, os.ModePerm)
|
||||
err = os.WriteFile(fullConfigFilePath, configFileMarshalled, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writeConfigFile: Unable to write to file [err=%s]", err)
|
||||
}
|
||||
|
@@ -20,6 +20,9 @@ func PrintErrorAndExit(exitCode int, err error, messages ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
supportMsg := fmt.Sprintf("\n\nIf this issue continues, get support at https://infisical.com/slack")
|
||||
fmt.Fprintln(os.Stderr, supportMsg)
|
||||
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
|
@@ -76,10 +76,31 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
|
||||
return nil, fmt.Errorf("unable to get your encrypted workspace key. [err=%v]", err)
|
||||
}
|
||||
|
||||
encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey)
|
||||
encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey)
|
||||
encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce)
|
||||
currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(receiversPrivateKey)
|
||||
encryptedWorkspaceKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey)
|
||||
if err != nil {
|
||||
HandleError(err, "Unable to get bytes represented by the base64 for encryptedWorkspaceKey")
|
||||
}
|
||||
|
||||
encryptedWorkspaceKeySenderPublicKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey)
|
||||
if err != nil {
|
||||
HandleError(err, "Unable to get bytes represented by the base64 for encryptedWorkspaceKeySenderPublicKey")
|
||||
}
|
||||
|
||||
encryptedWorkspaceKeyNonce, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce)
|
||||
if err != nil {
|
||||
HandleError(err, "Unable to get bytes represented by the base64 for encryptedWorkspaceKeyNonce")
|
||||
}
|
||||
|
||||
currentUsersPrivateKey, err := base64.StdEncoding.DecodeString(receiversPrivateKey)
|
||||
if err != nil {
|
||||
HandleError(err, "Unable to get bytes represented by the base64 for currentUsersPrivateKey")
|
||||
}
|
||||
|
||||
if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 {
|
||||
log.Debugf("Missing credentials for generating plainTextEncryptionKey: [currentUsersPrivateKey=%s] [encryptedWorkspaceKeySenderPublicKey=%s]", currentUsersPrivateKey, encryptedWorkspaceKeySenderPublicKey)
|
||||
PrintErrorMessageAndExit("Some required user credentials are missing to generate your [plainTextEncryptionKey]. Please run [infisical login] then try again")
|
||||
}
|
||||
|
||||
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
|
||||
|
||||
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
|
||||
@@ -131,6 +152,10 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if params.WorkspaceId != "" {
|
||||
workspaceFile.WorkspaceId = params.WorkspaceId
|
||||
}
|
||||
|
||||
// Verify environment
|
||||
err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials)
|
||||
if err != nil {
|
||||
@@ -415,7 +440,7 @@ func WriteBackupSecrets(workspace string, environment string, encryptionKey []by
|
||||
}
|
||||
|
||||
listOfSecretsMarshalled, _ := json.Marshal(encryptedSecrets)
|
||||
err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, os.ModePerm)
|
||||
err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WriteBackupSecrets: Unable to write backup secrets to file [err=%s]", err)
|
||||
}
|
||||
|
4
docs/api-reference/endpoints/service-tokens/get.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get"
|
||||
openapi: "GET /api/v2/service-token/"
|
||||
---
|
@@ -2,14 +2,24 @@
|
||||
title: "Authentication"
|
||||
---
|
||||
|
||||
To authenticate requests with Infisical, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. You can obtain an API key in User Settings > API Keys
|
||||
To authenticate requests with Infisical, you can either use an API Key or [Infisical Token](../../../getting-started/dashboard/token); certain endpoints will accept either one or both.
|
||||
- API Key: This general-purpose authentication token provides user access to most endpoints in this reference.
|
||||
- [Infisical Token](../../../getting-started/dashboard/token): This authentication token (also referred to as the service token) is scoped to a specific project and environment and used for CRUD secret operations.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="API Key">
|
||||
To authenticate requests with Infisical using the API Key, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform.
|
||||
|
||||
You can obtain an API key in User Settings > API Keys
|
||||
|
||||

|
||||

|
||||

|
||||
</Accordion>
|
||||
<Accordion title="Infisical Token">
|
||||
To authenticate requests with Infisical using the Infisical Token, you must include your Infisical Token in the `Authorization` header of HTTP requests made to the platform with the value `Bearer st.<rest_of_your_infisical_token>`.
|
||||
|
||||
<Info>
|
||||
It's important to keep your API key secure, as it grants access to your
|
||||
secrets in Infisical. For added security, set a reasonable expiration time and
|
||||
rotate your API key on a regular basis.
|
||||
</Info>
|
||||
You can obtain an Infisical Token in Project Settings > Service Tokens.
|
||||
|
||||

|
||||
</Accordion>
|
||||
</AccordionGroup>
|
@@ -2,48 +2,48 @@
|
||||
title: "Create secrets"
|
||||
---
|
||||
|
||||
In this example, we demonstrate how to add secrets to a project and environment.
|
||||
In this example, we demonstrate how to add secrets to a project and environment using an Infisical Token.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
- Create an [Infisical Token](../../../getting-started/dashboard/token) for your project and environment.
|
||||
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
|
||||
|
||||
## Flow
|
||||
|
||||
1. [Get your (encrypted) private key](/api-reference/endpoints/users/me).
|
||||
2. Decrypt your (encrypted) private key with your password.
|
||||
3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key)
|
||||
4. Decrypt the (encrypted) project key with your private key.
|
||||
5. Encrypt your secret(s) with the project key.
|
||||
6. [Send (encrypted) secret(s) to the Infical API](/api-reference/endpoints/secrets/create)
|
||||
1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key.
|
||||
2. Decrypt the (encrypted) project key with the key from your Infisical Token.
|
||||
3. Encrypt your secret(s) with the project key
|
||||
4. [Send (encrypted) secret(s) to Infical](/api-reference/endpoints/secrets/create)
|
||||
|
||||
## Example
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Javascript">
|
||||
```js
|
||||
const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
const nacl = require('tweetnacl');
|
||||
|
||||
const BASE_URL = 'https://app.infisical.com';
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const BLOCK_SIZE_BYTES = 16;
|
||||
|
||||
const encrypt = (
|
||||
text,
|
||||
secret
|
||||
) => {
|
||||
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
|
||||
const encrypt = ({ text, secret }) => {
|
||||
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
|
||||
|
||||
let ciphertext = cipher.update(text, 'utf8', 'base64');
|
||||
ciphertext += cipher.final('base64');
|
||||
return {
|
||||
ciphertext,
|
||||
iv: iv.toString('base64'),
|
||||
tag: cipher.getAuthTag().toString('base64')
|
||||
};
|
||||
let ciphertext = cipher.update(text, 'utf8', 'base64');
|
||||
ciphertext += cipher.final('base64');
|
||||
return {
|
||||
ciphertext,
|
||||
iv: iv.toString('base64'),
|
||||
tag: cipher.getAuthTag().toString('base64')
|
||||
};
|
||||
}
|
||||
|
||||
const decrypt = (ciphertext, iv, tag, secret) => {
|
||||
const decrypt = ({ ciphertext, iv, tag, secret}) => {
|
||||
const decipher = crypto.createDecipheriv(
|
||||
ALGORITHM,
|
||||
secret,
|
||||
@@ -58,95 +58,190 @@ const decrypt = (ciphertext, iv, tag, secret) => {
|
||||
}
|
||||
|
||||
const createSecrets = async () => {
|
||||
const API_KEY = 'your_api_key';
|
||||
const PSWD = 'your_pswd';
|
||||
const WORKSPACE_ID = 'your_workspace_id';
|
||||
const serviceToken = '';
|
||||
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
|
||||
|
||||
const secretType = 'shared'; // 'shared' or 'personal'
|
||||
const secretKey = 'some_key';
|
||||
const secretValue = 'some_value';
|
||||
const secretComment = 'some_comment';
|
||||
|
||||
const SECRET_KEY = 'SOME_KEY';
|
||||
const SECRET_VALUE = 'SOME_VALUE';
|
||||
// 1. Get your Infisical Token data
|
||||
const { data: serviceTokenData } = await axios.get(
|
||||
`${BASE_URL}/api/v2/service-token`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 1. get (encrypted) private key
|
||||
const user = await axios.get(
|
||||
'https://api.infisical.com/api/v2/users/me', {
|
||||
headers: {
|
||||
'X-API-KEY': API_KEY
|
||||
}
|
||||
}
|
||||
);
|
||||
// 2. Decrypt the (encrypted) project key with the key from your Infisical Token
|
||||
const projectKey = decrypt({
|
||||
ciphertext: serviceTokenData.encryptedKey,
|
||||
iv: serviceTokenData.iv,
|
||||
tag: serviceTokenData.tag,
|
||||
secret: serviceTokenSecret
|
||||
});
|
||||
|
||||
// 3. Encrypt your secret(s) with the project key
|
||||
const {
|
||||
ciphertext: secretKeyCiphertext,
|
||||
iv: secretKeyIV,
|
||||
tag: secretKeyTag
|
||||
} = encrypt({
|
||||
text: secretKey,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
// 2. decrypt your (encrypted) private key with your password
|
||||
const privateKey = decrypt({
|
||||
ciphertext: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
tag: user.tag,
|
||||
secret: PSWD.slice(0, 32).padStart(32, '0');
|
||||
});
|
||||
const {
|
||||
ciphertext: secretValueCiphertext,
|
||||
iv: secretValueIV,
|
||||
tag: secretValueTag
|
||||
} = encrypt({
|
||||
text: secretValue,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
// 3. get the (encrypted) project key for the project
|
||||
const encryptedProjectKey = await axios.get(
|
||||
`https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, {
|
||||
headers: {
|
||||
'X-API-KEY': API_KEY
|
||||
}
|
||||
}
|
||||
);
|
||||
const {
|
||||
ciphertext: secretCommentCiphertext,
|
||||
iv: secretCommentIV,
|
||||
tag: secretCommentTag
|
||||
} = encrypt({
|
||||
text: secretComment,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
const secret = {
|
||||
type: secretType,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag
|
||||
}
|
||||
|
||||
// 4. decrypt the project key with your private key
|
||||
const projectKey = nacl.box.open(
|
||||
util.decodeBase64(encryptedProjectKey),
|
||||
util.decodeBase64(projectKey.nonce),
|
||||
util.decodeBase64(projectKey.sender.publicKey),
|
||||
util.decodeBase64(privateKey)
|
||||
);
|
||||
|
||||
// 5. encrypt your secret(s) with the project key
|
||||
const {
|
||||
ciphertext: secretKeyCiphertext,
|
||||
iv: secretKeyIV,
|
||||
tag: secretKeyTag
|
||||
} = encrypt(SECRET_KEY, projectKey);
|
||||
|
||||
const {
|
||||
ciphertext: secretValueCiphertext,
|
||||
iv: secretValueIV,
|
||||
tag: secretValueTag
|
||||
} = encrypt(SECRET_VALUE, projectKey);
|
||||
|
||||
const secret = {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag
|
||||
}
|
||||
|
||||
// 6. Send (encrypted) secret(s) to the Infisical API
|
||||
await axios.post(
|
||||
`https://api.infisical.com/api/v2/secrets`,
|
||||
{
|
||||
workspaceId: WORKSPACE_ID,
|
||||
environment: 'dev',
|
||||
secrets: secret
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'X-API-KEY': API_KEY
|
||||
}
|
||||
}
|
||||
);
|
||||
// 4. Send (encrypted) secret(s) to Infisical
|
||||
await axios.post(
|
||||
`${BASE_URL}/api/v2/secrets`,
|
||||
{
|
||||
workspaceId: serviceTokenData.workspace,
|
||||
environment: serviceTokenData.environment,
|
||||
secrets: [secret]
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createSecrets();
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Python">
|
||||
```Python
|
||||
import base64
|
||||
import requests
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
|
||||
|
||||
BASE_URL = "https://app.infisical.com"
|
||||
BLOCK_SIZE_BYTES = 16
|
||||
|
||||
|
||||
def encrypt(text, secret):
|
||||
iv = get_random_bytes(BLOCK_SIZE_BYTES)
|
||||
secret = bytes(secret, "utf-8")
|
||||
cipher = AES.new(secret, AES.MODE_GCM, iv)
|
||||
ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8"))
|
||||
return {
|
||||
"ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"),
|
||||
"tag": base64.standard_b64encode(tag).decode("utf-8"),
|
||||
"iv": base64.standard_b64encode(iv).decode("utf-8"),
|
||||
}
|
||||
|
||||
|
||||
def decrypt(ciphertext, iv, tag, secret):
|
||||
secret = bytes(secret, "utf-8")
|
||||
iv = base64.standard_b64decode(iv)
|
||||
tag = base64.standard_b64decode(tag)
|
||||
ciphertext = base64.standard_b64decode(ciphertext)
|
||||
|
||||
cipher = AES.new(secret, AES.MODE_GCM, iv)
|
||||
cipher.update(tag)
|
||||
cleartext = cipher.decrypt(ciphertext).decode("utf-8")
|
||||
return cleartext
|
||||
|
||||
|
||||
def create_secrets():
|
||||
service_token = "your_service_token"
|
||||
service_token_secret = service_token[service_token.rindex(".") + 1 :]
|
||||
|
||||
secret_type = "shared" # "shared or "personal"
|
||||
secret_key = "some_key"
|
||||
secret_value = "some_value"
|
||||
secret_comment = "some_comment"
|
||||
|
||||
# 1. Get your Infisical Token data
|
||||
service_token_data = requests.get(
|
||||
f"{BASE_URL}/api/v2/service-token",
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
).json()
|
||||
|
||||
# 2. Decrypt the (encrypted) project key with the key from your Infisical Token
|
||||
project_key = decrypt(
|
||||
ciphertext=service_token_data["encryptedKey"],
|
||||
iv=service_token_data["iv"],
|
||||
tag=service_token_data["tag"],
|
||||
secret=service_token_secret,
|
||||
)
|
||||
|
||||
# 3. Encrypt your secret(s) with the project key
|
||||
encrypted_key_data = encrypt(text=secret_key, secret=project_key)
|
||||
encrypted_value_data = encrypt(text=secret_value, secret=project_key)
|
||||
encrypted_comment_data = encrypt(text=secret_comment, secret=project_key)
|
||||
|
||||
secret = {
|
||||
"type": secret_type,
|
||||
"secretKeyCiphertext": encrypted_key_data["ciphertext"],
|
||||
"secretKeyIV": encrypted_key_data["iv"],
|
||||
"secretKeyTag": encrypted_key_data["tag"],
|
||||
"secretValueCiphertext": encrypted_value_data["ciphertext"],
|
||||
"secretValueIV": encrypted_value_data["iv"],
|
||||
"secretValueTag": encrypted_value_data["tag"],
|
||||
"secretCommentCiphertext": encrypted_comment_data["ciphertext"],
|
||||
"secretCommentIV": encrypted_comment_data["iv"],
|
||||
"secretCommentTag": encrypted_comment_data["tag"],
|
||||
}
|
||||
|
||||
# 4. Send (encrypted) secret (s) to Infisical
|
||||
requests.post(
|
||||
f"{BASE_URL}/api/v2/secrets",
|
||||
json={
|
||||
"workspaceId": service_token_data["workspace"],
|
||||
"environment": service_token_data["environment"],
|
||||
"secrets": [secret],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
)
|
||||
|
||||
|
||||
create_secrets()
|
||||
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Info>
|
||||
This example uses [TweetNaCl.js](https://tweetnacl.js.org/#/), a port of
|
||||
TweetNacl/Nacl, to perform asymmeric decryption of the project key but there
|
||||
are ports of NaCl available in every major language.
|
||||
</Info>
|
||||
<Tip>
|
||||
It can be useful to perform steps 1-4 ahead of time and store away your
|
||||
private key (and even project key) for later use. The Infisical CLI works by
|
||||
securely storing your private key via your OS keyring.
|
||||
</Tip>
|
||||
</Info>
|
@@ -7,24 +7,30 @@ In this example, we demonstrate how to delete secrets
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
- Create either an [API Key](/api-reference/overview/authentication) or [Infisical Token](../../../getting-started/dashboard/token) for your project and environment.
|
||||
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
|
||||
|
||||
## Example
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Javascript">
|
||||
```js
|
||||
const axios = require('axios');
|
||||
const BASE_URL = 'https://app.infisical.com';
|
||||
|
||||
const deleteSecrets = async () => {
|
||||
const API_KEY = "your_api_key";
|
||||
const SECRET_ID = "ID"; // ID of secret to delete
|
||||
const serviceToken = 'your_service_token';
|
||||
const secretId = 'id_of_secret_to_delete';
|
||||
|
||||
// 6. Send ID(s) of secret(s) to delete to the Infisical API
|
||||
await axios.delete(
|
||||
`https://api.infisical.com/api/v2/secrets`,
|
||||
`${BASE_URL}/api/v2/secrets`,
|
||||
{
|
||||
secretIds: SECRET_ID,
|
||||
secretIds: [secretId],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"X-API-KEY": API_KEY,
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -32,3 +38,33 @@ const deleteSecrets = async () => {
|
||||
|
||||
deleteSecrets();
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Python">
|
||||
```Python
|
||||
import requests
|
||||
|
||||
BASE_URL = "https://app.infisical.com"
|
||||
|
||||
|
||||
def delete_secrets():
|
||||
service_token = "<your_service_token>"
|
||||
secret_id = "id_of_secret_to_delete"
|
||||
|
||||
# Send ID(s) of secret(s) to delete to the Infisical API
|
||||
requests.delete(
|
||||
f"{BASE_URL}/api/v2/secrets",
|
||||
json={"secretIds": [secret_id]},
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
)
|
||||
|
||||
|
||||
delete_secrets()
|
||||
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<Info>
|
||||
If using an `API_KEY` to authenticate with the Infisical API, then you should include it in the `X_API_KEY` header.
|
||||
</Info>
|
||||
|
||||
|
@@ -2,48 +2,33 @@
|
||||
title: "Retrieve secrets"
|
||||
---
|
||||
|
||||
In this example, we demonstrate how to retrieve secrets from a project and environment.
|
||||
In this example, we demonstrate how to retrieve secrets from a project and environment using an Infisical Token.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
|
||||
- Create an [Infisical Token](../../../getting-started/dashboard/token) for your project and environment.
|
||||
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
|
||||
|
||||
## Flow
|
||||
|
||||
1. [Get your (encrypted) private key.](/api-reference/endpoints/users/me)
|
||||
2. Decrypt your (encrypted) private key with your password.
|
||||
3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key)
|
||||
4. Decrypt the (encrypted) project key with your private key.
|
||||
5. [Get secrets for a project and environment.](/api-reference/endpoints/secrets/read)
|
||||
6. Decrypt the (encrypted) secrets
|
||||
1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key.
|
||||
2. [Get secrets for your project and environment](/api-reference/endpoints/secrets/read).
|
||||
3. Decrypt the (encrypted) project key with the key from your Infisical Token.
|
||||
4. Decrypt the (encrypted) secrets
|
||||
|
||||
## Example
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Javascript">
|
||||
```js
|
||||
const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = 'https://app.infisical.com';
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const BLOCK_SIZE_BYTES = 16;
|
||||
|
||||
const encrypt = (
|
||||
text,
|
||||
secret
|
||||
) => {
|
||||
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
|
||||
|
||||
let ciphertext = cipher.update(text, 'utf8', 'base64');
|
||||
ciphertext += cipher.final('base64');
|
||||
return {
|
||||
ciphertext,
|
||||
iv: iv.toString('base64'),
|
||||
tag: cipher.getAuthTag().toString('base64')
|
||||
};
|
||||
}
|
||||
|
||||
const decrypt = (ciphertext, iv, tag, secret) => {
|
||||
const decrypt = ({ ciphertext, iv, tag, secret}) => {
|
||||
const decipher = crypto.createDecipheriv(
|
||||
ALGORITHM,
|
||||
secret,
|
||||
@@ -51,73 +36,63 @@ const decrypt = (ciphertext, iv, tag, secret) => {
|
||||
);
|
||||
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
||||
|
||||
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
||||
cleartext += decipher.final('utf8');
|
||||
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
||||
cleartext += decipher.final('utf8');
|
||||
|
||||
return cleartext;
|
||||
return cleartext;
|
||||
}
|
||||
|
||||
const retrieveSecrets = async () => {
|
||||
const API_KEY = 'your_api_key';
|
||||
const PSWD = 'your_pswd';
|
||||
const WORKSPACE_ID = 'your_workspace_id';
|
||||
const getSecrets = async () => {
|
||||
const serviceToken = 'your_service_token';
|
||||
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
|
||||
|
||||
// 1. get (encrypted) private key
|
||||
const user = await axios.get(
|
||||
'https://api.infisical.com/api/v2/users/me', {
|
||||
headers: {
|
||||
'X-API-KEY': API_KEY
|
||||
}
|
||||
}
|
||||
);
|
||||
// 1. Get your Infisical Token data
|
||||
const { data: serviceTokenData } = await axios.get(
|
||||
`${BASE_URL}/api/v2/service-token`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 2. decrypt your (encrypted) private key with your password
|
||||
const privateKey = decrypt({
|
||||
ciphertext: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
tag: user.tag,
|
||||
secret: PSWD.slice(0, 32).padStart(32, '0');
|
||||
});
|
||||
// 2. Get secrets for your project and environment
|
||||
const { data } = await axios.get(
|
||||
`${BASE_URL}/api/v2/secrets?${new URLSearchParams({
|
||||
environment: serviceTokenData.environment,
|
||||
workspaceId: serviceTokenData.workspace
|
||||
})}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 3. get the (encrypted) project key for the project
|
||||
const encryptedProjectKey = await axios.get(
|
||||
`https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, {
|
||||
headers: {
|
||||
'X-API-KEY': API_KEY
|
||||
}
|
||||
}
|
||||
);
|
||||
const encryptedSecrets = data.secrets;
|
||||
|
||||
// 4. decrypt the project key with your private key
|
||||
const projectKey = nacl.box.open(
|
||||
util.decodeBase64(encryptedProjectKey),
|
||||
util.decodeBase64(projectKey.nonce),
|
||||
util.decodeBase64(projectKey.sender.publicKey),
|
||||
util.decodeBase64(privateKey)
|
||||
);
|
||||
// 3. Decrypt the (encrypted) project key with the key from your Infisical Token
|
||||
const projectKey = decrypt({
|
||||
ciphertext: serviceTokenData.encryptedKey,
|
||||
iv: serviceTokenData.iv,
|
||||
tag: serviceTokenData.tag,
|
||||
secret: serviceTokenSecret
|
||||
});
|
||||
|
||||
// 5. get (encrypted) secrets for a project and environment.
|
||||
const encryptedSecrets = await axios.get(
|
||||
'https://api.infisical.com/api/v2/secrets', {
|
||||
headers: {
|
||||
'X-API-KEY': API_KEY
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 6. decrypt the (encrypted) secrets
|
||||
const secrets = encryptedSecrets.map((encryptedSecret) => {
|
||||
// 4. Decrypt the (encrypted) secrets
|
||||
const secrets = encryptedSecrets.map((secret) => {
|
||||
const secretKey = decrypt({
|
||||
ciphertext: encryptedSecret.secretKeyCiphertext,
|
||||
iv: encryptedSecret.secretKeyIV,
|
||||
tag: encryptedSecret.secretKeyTag
|
||||
secret: projectKey
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
const secretValue = decrypt({
|
||||
ciphertext: encryptedSecret.secretValueCiphertext,
|
||||
iv: encryptedSecret.secretValueIV,
|
||||
tag: encryptedSecret.secretValueTag
|
||||
secret: projectKey
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
return ({
|
||||
@@ -125,18 +100,102 @@ const retrieveSecrets = async () => {
|
||||
secretValue
|
||||
});
|
||||
});
|
||||
|
||||
console.log('secrets: ', secrets);
|
||||
}
|
||||
|
||||
retrieveSecrets();
|
||||
getSecrets();
|
||||
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Python">
|
||||
```Python
|
||||
import requests
|
||||
import base64
|
||||
from Cryptodome.Cipher import AES
|
||||
|
||||
|
||||
BASE_URL = "http://app.infisical.com"
|
||||
|
||||
|
||||
def decrypt(ciphertext, iv, tag, secret):
|
||||
secret = bytes(secret, "utf-8")
|
||||
iv = base64.standard_b64decode(iv)
|
||||
tag = base64.standard_b64decode(tag)
|
||||
ciphertext = base64.standard_b64decode(ciphertext)
|
||||
|
||||
cipher = AES.new(secret, AES.MODE_GCM, iv)
|
||||
cipher.update(tag)
|
||||
cleartext = cipher.decrypt(ciphertext).decode("utf-8")
|
||||
return cleartext
|
||||
|
||||
|
||||
def get_secrets():
|
||||
service_token = "your_service_token"
|
||||
service_token_secret = service_token[service_token.rindex(".") + 1 :]
|
||||
|
||||
# 1. Get your Infisical Token data
|
||||
service_token_data = requests.get(
|
||||
f"{BASE_URL}/api/v2/service-token",
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
).json()
|
||||
|
||||
# 2. Get secrets for your project and environment
|
||||
data = requests.get(
|
||||
f"{BASE_URL}/api/v2/secrets",
|
||||
params={
|
||||
"environment": service_token_data["environment"],
|
||||
"workspaceId": service_token_data["workspace"],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
).json()
|
||||
|
||||
encrypted_secrets = data["secrets"]
|
||||
|
||||
# 3. Decrypt the (encrypted) project key with the key from your Infisical Token
|
||||
project_key = decrypt(
|
||||
ciphertext=service_token_data["encryptedKey"],
|
||||
iv=service_token_data["iv"],
|
||||
tag=service_token_data["tag"],
|
||||
secret=service_token_secret,
|
||||
)
|
||||
|
||||
# 4. Decrypt the (encrypted) secrets
|
||||
secrets = []
|
||||
for secret in encrypted_secrets:
|
||||
secret_key = decrypt(
|
||||
ciphertext=secret["secretKeyCiphertext"],
|
||||
iv=secret["secretKeyIV"],
|
||||
tag=secret["secretKeyTag"],
|
||||
secret=project_key,
|
||||
)
|
||||
|
||||
secret_value = decrypt(
|
||||
ciphertext=secret["secretValueCiphertext"],
|
||||
iv=secret["secretValueIV"],
|
||||
tag=secret["secretValueTag"],
|
||||
secret=project_key,
|
||||
)
|
||||
|
||||
secrets.append(
|
||||
{
|
||||
"secret_key": secret_key,
|
||||
"secret_value": secret_value,
|
||||
}
|
||||
)
|
||||
|
||||
print("secrets:", secrets)
|
||||
|
||||
|
||||
get_secrets()
|
||||
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Info>
|
||||
This example uses [TweetNaCl.js](https://tweetnacl.js.org/#/), a port of
|
||||
TweetNacl/Nacl, to perform asymmeric decryption of the project key but there
|
||||
are ports of NaCl available in every major language.
|
||||
</Info>
|
||||
<Tip>
|
||||
It can be useful to perform steps 1-4 ahead of time and store away your
|
||||
private key (and even project key) for later use. The Infisical CLI works by
|
||||
securely storing your private key via your OS keyring.
|
||||
</Tip>
|
||||
</Info>
|
@@ -2,48 +2,47 @@
|
||||
title: "Update secrets"
|
||||
---
|
||||
|
||||
In this example, we demonstrate how to update secrets
|
||||
In this example, we demonstrate how to update secrets using an Infisical Token.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
- Create an [Infisical Token](../../../getting-started/dashboard/token) for your project and environment.
|
||||
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
|
||||
|
||||
## Flow
|
||||
|
||||
1. [Get your (encrypted) private key.](/api-reference/endpoints/users/me)
|
||||
2. Decrypt your (encrypted) private key with your password.
|
||||
3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key)
|
||||
4. Decrypt the (encrypted) project key with your private key.
|
||||
5. Encrypt your secret(s) with the project key.
|
||||
6. [Send (encrypted) updated secret(s) to the Infical API.](/api-reference/endpoints/secrets/update)
|
||||
1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key.
|
||||
2. Decrypt the (encrypted) project key with the key from your Infisical Token.
|
||||
3. Encrypt your updated secret(s) with the project key
|
||||
4. [Send (encrypted) updated secret(s) to Infical](/api-reference/endpoints/secrets/update)
|
||||
|
||||
## Example
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Javascript">
|
||||
```js
|
||||
const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = 'https://app.infisical.com';
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const BLOCK_SIZE_BYTES = 16;
|
||||
|
||||
const encrypt = (
|
||||
text,
|
||||
secret
|
||||
) => {
|
||||
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
|
||||
const encrypt = ({ text, secret }) => {
|
||||
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
|
||||
|
||||
let ciphertext = cipher.update(text, 'utf8', 'base64');
|
||||
ciphertext += cipher.final('base64');
|
||||
return {
|
||||
ciphertext,
|
||||
iv: iv.toString('base64'),
|
||||
tag: cipher.getAuthTag().toString('base64')
|
||||
};
|
||||
let ciphertext = cipher.update(text, 'utf8', 'base64');
|
||||
ciphertext += cipher.final('base64');
|
||||
return {
|
||||
ciphertext,
|
||||
iv: iv.toString('base64'),
|
||||
tag: cipher.getAuthTag().toString('base64')
|
||||
};
|
||||
}
|
||||
|
||||
const decrypt = (ciphertext, iv, tag, secret) => {
|
||||
const decrypt = ({ ciphertext, iv, tag, secret}) => {
|
||||
const decipher = crypto.createDecipheriv(
|
||||
ALGORITHM,
|
||||
secret,
|
||||
@@ -58,95 +57,188 @@ const decrypt = (ciphertext, iv, tag, secret) => {
|
||||
}
|
||||
|
||||
const updateSecrets = async () => {
|
||||
const API_KEY = 'your_api_key';
|
||||
const PSWD = 'your_pswd';
|
||||
const WORKSPACE_ID = 'your_workspace_id';
|
||||
const serviceToken = 'your_service_token';
|
||||
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
|
||||
|
||||
const secretId = 'id_of_secret_to_update';
|
||||
const secretKey = 'some_key';
|
||||
const secretValue = 'updated_value';
|
||||
const secretComment = 'updated_comment';
|
||||
|
||||
const SECRET_ID = 'ID' // ID of secret to update
|
||||
const SECRET_KEY = 'SOME_KEY';
|
||||
const SECRET_VALUE = 'SOME_VALUE';
|
||||
// 1. Get your Infisical Token data
|
||||
const { data: serviceTokenData } = await axios.get(
|
||||
`${BASE_URL}/api/v2/service-token`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 1. get (encrypted) private key
|
||||
const user = await axios.get(
|
||||
'https://api.infisical.com/api/v2/users/me', {
|
||||
headers: {
|
||||
'X-API-KEY': API_KEY
|
||||
}
|
||||
}
|
||||
);
|
||||
// 2. Decrypt the (encrypted) project key with the key from your Infisical Token
|
||||
const projectKey = decrypt({
|
||||
ciphertext: serviceTokenData.encryptedKey,
|
||||
iv: serviceTokenData.iv,
|
||||
tag: serviceTokenData.tag,
|
||||
secret: serviceTokenSecret
|
||||
});
|
||||
|
||||
// 3. Encrypt your updated secret(s) with the project key
|
||||
const {
|
||||
ciphertext: secretKeyCiphertext,
|
||||
iv: secretKeyIV,
|
||||
tag: secretKeyTag
|
||||
} = encrypt({
|
||||
text: secretKey,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
// 2. decrypt your (encrypted) private key with your password
|
||||
const privateKey = decrypt({
|
||||
ciphertext: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
tag: user.tag,
|
||||
secret: PSWD.slice(0, 32).padStart(32, '0');
|
||||
});
|
||||
const {
|
||||
ciphertext: secretValueCiphertext,
|
||||
iv: secretValueIV,
|
||||
tag: secretValueTag
|
||||
} = encrypt({
|
||||
text: secretValue,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
// 3. get the (encrypted) project key for the project
|
||||
const encryptedProjectKey = await axios.get(
|
||||
`https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, {
|
||||
headers: {
|
||||
'X-API-KEY': API_KEY
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 4. decrypt the project key with your private key
|
||||
const projectKey = nacl.box.open(
|
||||
util.decodeBase64(encryptedProjectKey),
|
||||
util.decodeBase64(projectKey.nonce),
|
||||
util.decodeBase64(projectKey.sender.publicKey),
|
||||
util.decodeBase64(privateKey)
|
||||
);
|
||||
|
||||
// 5. encrypt your secret(s) with the project key
|
||||
const {
|
||||
ciphertext: secretKeyCiphertext,
|
||||
iv: secretKeyIV,
|
||||
tag: secretKeyTag
|
||||
} = encrypt(SECRET_KEY, projectKey);
|
||||
|
||||
const {
|
||||
ciphertext: secretValueCiphertext,
|
||||
iv: secretValueIV,
|
||||
tag: secretValueTag
|
||||
} = encrypt(SECRET_VALUE, projectKey);
|
||||
|
||||
const secret = {
|
||||
id: SECRET_ID,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag
|
||||
}
|
||||
|
||||
// 6. Send (encrypted) secret(s) to the Infisical API
|
||||
await axios.patch(
|
||||
`https://api.infisical.com/api/v2/secrets`,
|
||||
{
|
||||
secrets: secret
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'X-API-KEY': API_KEY
|
||||
}
|
||||
}
|
||||
);
|
||||
const {
|
||||
ciphertext: secretCommentCiphertext,
|
||||
iv: secretCommentIV,
|
||||
tag: secretCommentTag
|
||||
} = encrypt({
|
||||
text: secretComment,
|
||||
secret: projectKey
|
||||
});
|
||||
|
||||
const secret = {
|
||||
id: secretId,
|
||||
workspace: serviceTokenData.workspace,
|
||||
environment: serviceTokenData.environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag
|
||||
}
|
||||
|
||||
// 4. Send (encrypted) updated secret(s) to Infisical
|
||||
await axios.patch(
|
||||
`${BASE_URL}/api/v2/secrets`,
|
||||
{
|
||||
secrets: [secret]
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updateSecrets();
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Python">
|
||||
```Python
|
||||
import base64
|
||||
import requests
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
|
||||
|
||||
BASE_URL = "https://app.infisical.com"
|
||||
BLOCK_SIZE_BYTES = 16
|
||||
|
||||
|
||||
def encrypt(text, secret):
|
||||
iv = get_random_bytes(BLOCK_SIZE_BYTES)
|
||||
secret = bytes(secret, "utf-8")
|
||||
cipher = AES.new(secret, AES.MODE_GCM, iv)
|
||||
ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8"))
|
||||
return {
|
||||
"ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"),
|
||||
"tag": base64.standard_b64encode(tag).decode("utf-8"),
|
||||
"iv": base64.standard_b64encode(iv).decode("utf-8"),
|
||||
}
|
||||
|
||||
|
||||
def decrypt(ciphertext, iv, tag, secret):
|
||||
secret = bytes(secret, "utf-8")
|
||||
iv = base64.standard_b64decode(iv)
|
||||
tag = base64.standard_b64decode(tag)
|
||||
ciphertext = base64.standard_b64decode(ciphertext)
|
||||
|
||||
cipher = AES.new(secret, AES.MODE_GCM, iv)
|
||||
cipher.update(tag)
|
||||
cleartext = cipher.decrypt(ciphertext).decode("utf-8")
|
||||
return cleartext
|
||||
|
||||
|
||||
def update_secret():
|
||||
service_token = "your_service_token"
|
||||
service_token_secret = service_token[service_token.rindex(".") + 1 :]
|
||||
|
||||
secret_id = "id_of_secret_to_update"
|
||||
secret_key = "some_key"
|
||||
secret_value = "updated_value"
|
||||
secret_comment = "updated_comment"
|
||||
|
||||
# 1. Get your Infisical Token data
|
||||
service_token_data = requests.get(
|
||||
f"{BASE_URL}/api/v2/service-token",
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
).json()
|
||||
|
||||
# 2. Decrypt the (encrypted) project key with the key from your Infisical Token
|
||||
project_key = decrypt(
|
||||
ciphertext=service_token_data["encryptedKey"],
|
||||
iv=service_token_data["iv"],
|
||||
tag=service_token_data["tag"],
|
||||
secret=service_token_secret,
|
||||
)
|
||||
|
||||
# 3. Encrypt your updated secret(s) with the project key
|
||||
encrypted_key_data = encrypt(text=secret_key, secret=project_key)
|
||||
encrypted_value_data = encrypt(text=secret_value, secret=project_key)
|
||||
encrypted_comment_data = encrypt(text=secret_comment, secret=project_key)
|
||||
|
||||
secret = {
|
||||
"id": secret_id,
|
||||
"workspace": service_token_data["workspace"],
|
||||
"environment": service_token_data["environment"],
|
||||
"secretKeyCiphertext": encrypted_key_data["ciphertext"],
|
||||
"secretKeyIV": encrypted_key_data["iv"],
|
||||
"secretKeyTag": encrypted_key_data["tag"],
|
||||
"secretValueCiphertext": encrypted_value_data["ciphertext"],
|
||||
"secretValueIV": encrypted_value_data["iv"],
|
||||
"secretValueTag": encrypted_value_data["tag"],
|
||||
"secretCommentCiphertext": encrypted_comment_data["ciphertext"],
|
||||
"secretCommentIV": encrypted_comment_data["iv"],
|
||||
"secretCommentTag": encrypted_comment_data["tag"],
|
||||
}
|
||||
|
||||
# 4. Send (encrypted) updated secret(s) to Infisical
|
||||
requests.patch(
|
||||
f"{BASE_URL}/api/v2/secrets",
|
||||
json={"secrets": [secret]},
|
||||
headers={"Authorization": f"Bearer {service_token}"},
|
||||
)
|
||||
|
||||
|
||||
update_secret()
|
||||
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Info>
|
||||
This example uses [TweetNaCl.js](https://tweetnacl.js.org/#/), a port of
|
||||
TweetNacl/Nacl, to perform asymmeric decryption of the project key but there
|
||||
are ports of NaCl available in every major language.
|
||||
</Info>
|
||||
<Tip>
|
||||
It can be useful to perform steps 1-4 ahead of time and store away your
|
||||
private key (and even project key) for later use. The Infisical CLI works by
|
||||
securely storing your private key via your OS keyring.
|
||||
</Tip>
|
||||
</Info>
|
@@ -12,10 +12,11 @@ With the REST API, users can create, read, update, and delete secrets, as well a
|
||||
|
||||
Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview).
|
||||
|
||||
- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by the user's password before being sent off to the server during the account signup process.
|
||||
- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by protected keys that are encrypted by keys derived from Argon2id applied to the user's password before being sent off to the server during the account signup process.
|
||||
- Each (encrypted) secret belongs to a project and environment.
|
||||
- Each project has an (encrypted) project key used to encrypt the secrets within that project; Infisical stores copies of the project key, for each member of that project, encrypted under each member's public key.
|
||||
- Secrets are encrypted symmetrically by your copy of the project key belonging to the project containing.
|
||||
- Infisical Tokens contain a symmetric key that can be used to decrypt a copy of a project key from the [call to get the Infisical Token data](/api-reference/endpoints/service-tokens/get).
|
||||
- Infisical uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations.
|
||||
|
||||
<Info>
|
||||
|
@@ -11,31 +11,105 @@ infisical export [options]
|
||||
|
||||
Export environment variables from the platform into a file format.
|
||||
|
||||
## Options
|
||||
## Subcommands & flags
|
||||
|
||||
| Option | Description | Default value |
|
||||
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
|
||||
| `--projectId` | Only required if injecting via the [service token method](../token). If you are not using service token, the project id will be automatically retrieved from the `.infisical.json` located at the root of your local project. | `None` |
|
||||
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
|
||||
| `--format` | Format of the output file. Accepted values: `dotenv`, `dotenv-export`, `csv` and `json` | `dotenv` |
|
||||
<Accordion title="infisical export" defaultOpen="true">
|
||||
Use this command to export environment variables from the platform into a raw file formats
|
||||
|
||||
## Examples
|
||||
```bash
|
||||
$ infisical export
|
||||
|
||||
```bash
|
||||
# Export variables to a .env file
|
||||
infisical export > .env
|
||||
# Export variables to a .env file
|
||||
infisical export > .env
|
||||
|
||||
# Export variables to a .env file (with export keyword)
|
||||
infisical export --format=dotenv-export > .env
|
||||
# Export variables to a .env file (with export keyword)
|
||||
infisical export --format=dotenv-export > .env
|
||||
|
||||
# Export variables to a CSV file
|
||||
infisical export --format=csv > secrets.csv
|
||||
# Export variables to a CSV file
|
||||
infisical export --format=csv > secrets.csv
|
||||
|
||||
# Export variables to a JSON file
|
||||
infisical export --format=json > secrets.json
|
||||
# Export variables to a JSON file
|
||||
infisical export --format=json > secrets.json
|
||||
|
||||
# Export variables to a YAML file
|
||||
infisical export --format=yaml > secrets.yaml
|
||||
# Export variables to a YAML file
|
||||
infisical export --format=yaml > secrets.yaml
|
||||
```
|
||||
|
||||
```
|
||||
### Environment variables
|
||||
<Accordion title="INFISICAL_TOKEN">
|
||||
Used to fetch secrets via a [service token](/getting-started/dashboard/token) apposed to logged in credentials. Simply, export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_TOKEN=st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="INFISICAL_DISABLE_UPDATE_CHECK">
|
||||
Used to disable the check for new CLI versions. This can improve the time it takes to run this command. Recommended for production environments.
|
||||
|
||||
To use, simply export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_DISABLE_UPDATE_CHECK=true
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
### flags
|
||||
<Accordion title="--env">
|
||||
Used to set the environment that secrets are pulled from.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical export --env=prod
|
||||
```
|
||||
|
||||
Note: this flag only accepts environment slug names not the fully qualified name. To view the slug name of an environment, visit the project settings page.
|
||||
|
||||
default value: `dev`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--projectId">
|
||||
By default the project id is retrieved from the `.infisical.json` located at the root of your local project.
|
||||
This flag allows you to override this behavior by explicitly defining the project to fetch your secrets from.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
|
||||
infisical export --projectId=XXXXXXXXXXXXXX
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--expand">
|
||||
Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`)
|
||||
|
||||
Default value: `true`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--format">
|
||||
Format of the output file. Accepted values: `dotenv`, `dotenv-export`, `csv` and `json`
|
||||
|
||||
Default value: `dotenv`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--secret-overriding">
|
||||
Prioritizes personal secrets with the same name over shared secrets
|
||||
|
||||
Default value: `true`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--tags">
|
||||
When working with tags, you can use this flag to filter and retrieve only secrets that are associated with a specific tag(s).
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical run --tags=tag1,tag2,tag3 -- npm run dev
|
||||
```
|
||||
|
||||
Note: you must reference the tag by its slug name not its fully qualified name. Go to project settings to view all tag slugs.
|
||||
|
||||
By default, all secrets are fetched
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
@@ -40,7 +40,28 @@ Inject secrets from Infisical into your application process.
|
||||
$ infisical run -- npm run dev
|
||||
```
|
||||
|
||||
### flags
|
||||
### Environment variables
|
||||
<Accordion title="INFISICAL_TOKEN">
|
||||
Used to fetch secrets via a [service token](/getting-started/dashboard/token) apposed to logged in credentials. Simply, export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_TOKEN=st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="INFISICAL_DISABLE_UPDATE_CHECK">
|
||||
Used to disable the check for new CLI versions. This can improve the time it takes to run this command. Recommended for production environments.
|
||||
|
||||
To use, simply export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_DISABLE_UPDATE_CHECK=true
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
### Flags
|
||||
<Accordion title="--command">
|
||||
Pass secrets into multiple commands at once
|
||||
|
||||
@@ -79,4 +100,17 @@ Inject secrets from Infisical into your application process.
|
||||
Default value: `true`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--tags">
|
||||
When working with tags, you can use this flag to filter and retrieve only secrets that are associated with a specific tag(s).
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical run --tags=tag1,tag2,tag3 -- npm run dev
|
||||
```
|
||||
|
||||
Note: you must reference the tag by its slug name not its fully qualified name. Go to project settings to view all tag slugs.
|
||||
|
||||
By default, all secrets are fetched
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
@@ -18,7 +18,28 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
|
||||
$ infisical secrets
|
||||
```
|
||||
|
||||
### flags
|
||||
### Environment variables
|
||||
<Accordion title="INFISICAL_TOKEN">
|
||||
Used to fetch secrets via a [service token](/getting-started/dashboard/token) apposed to logged in credentials. Simply, export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_TOKEN=st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="INFISICAL_DISABLE_UPDATE_CHECK">
|
||||
Used to disable the check for new CLI versions. This can improve the time it takes to run this command. Recommended for production environments.
|
||||
|
||||
To use, simply export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_DISABLE_UPDATE_CHECK=true
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
### Flags
|
||||
<Accordion title="--expand">
|
||||
Parse shell parameter expansions in your secrets
|
||||
|
||||
|
@@ -45,19 +45,19 @@ The Infisical CLI provides a way to inject environment variables from the platfo
|
||||
<Tab title="Alpine">
|
||||
Install prerequisite
|
||||
```bash
|
||||
sudo apk add --no-cache bash sudo
|
||||
apk add --no-cache bash sudo
|
||||
```
|
||||
|
||||
Add Infisical repository
|
||||
```bash
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' \
|
||||
| sudo -E bash
|
||||
| bash
|
||||
```
|
||||
|
||||
Then install CLI
|
||||
```bash
|
||||
sudo apk update && sudo apk add infisical
|
||||
apk update && sudo apk add infisical
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
@@ -1,12 +1,12 @@
|
||||
---
|
||||
title: "Activity Logs"
|
||||
title: "Audit Logs"
|
||||
description: "See which events are triggered within your Infisical project."
|
||||
---
|
||||
|
||||
Activity logs record all actions going through Infisical including who performed which CRUD operations on environment variables and from what IP address. They help answer questions like:
|
||||
Audit logs record all actions going through Infisical including who performed which CRUD operations on environment variables and from what IP address. They help answer questions like:
|
||||
|
||||
- Who added or updated environment variables recently?
|
||||
- Did Bob read environment variables last week (if at all)?
|
||||
- What IP address was used for that action?
|
||||
|
||||

|
||||

|
||||
|
@@ -12,7 +12,7 @@ This is a non-exhaustive list of features that Infisical offers:
|
||||
- Sync secrets to platforms via integrations to platforms like GitHub, Vercel, and Netlify.
|
||||
- Rollback secrets to any point in time.
|
||||
- Rollback each secrets to any version.
|
||||
- Track actions through activity logs.
|
||||
- Track actions through audit logs.
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -27,18 +27,18 @@ We're building the future of secret management, one that's comprehensive and acc
|
||||
|
||||
| Feature | Status |
|
||||
| ------------------------------------- | ----------- |
|
||||
| Account recovery: Backup key | Done |
|
||||
| 2FA | Done |
|
||||
| Read/write access controls | Done |
|
||||
| Comparing secrets across environments | Done |
|
||||
| Integrations | Ongoing |
|
||||
| More hosting options | Ongoing |
|
||||
| 1-Click Deploys | Ongoing |
|
||||
| Account recovery: Backup key | Ongoing |
|
||||
| Access logs | Ongoing |
|
||||
| Account recovery: Member-assisted | Coming soon |
|
||||
| Slack & MS teams integrations | Coming soon |
|
||||
| Version control for secrets | Coming soon |
|
||||
| 2FA | Coming soon |
|
||||
| Restricted IPs | Coming soon |
|
||||
| Read/write access controls | Coming soon |
|
||||
| Secret rotation | Coming soon |
|
||||
| Comparing secrets across environments | Coming soon |
|
||||
|
||||
Interested in contributing? Check out the [guide](/contributing/overview).
|
||||
|
BIN
docs/images/integrations-azure-key-vault-create.png
Normal file
After Width: | Height: | Size: 189 KiB |
BIN
docs/images/integrations-azure-key-vault-vault-uri.png
Normal file
After Width: | Height: | Size: 352 KiB |
BIN
docs/images/integrations-azure-key-vault.png
Normal file
After Width: | Height: | Size: 379 KiB |
BIN
docs/images/integrations-gitlab-auth.png
Normal file
After Width: | Height: | Size: 330 KiB |
BIN
docs/images/integrations-gitlab-create.png
Normal file
After Width: | Height: | Size: 185 KiB |
BIN
docs/images/integrations-gitlab.png
Normal file
After Width: | Height: | Size: 394 KiB |
Before Width: | Height: | Size: 424 KiB After Width: | Height: | Size: 424 KiB |
@@ -1,34 +1,70 @@
|
||||
---
|
||||
title: "Gitlab Pipeline"
|
||||
title: "GitLab"
|
||||
description: "How to automatically sync secrets from Infisical into GitLab."
|
||||
---
|
||||
|
||||
To integrate Infisical secrets into your Gitlab CI/CD setup, three steps are required.
|
||||
Prerequisites:
|
||||
|
||||
## Generate service token
|
||||
To expose Infisical secrets in Gitlab CI/CD, you must generate a service token for the specific project and environment in Infisical. For instructions on how to generate a service token, refer to [this page](../../getting-started/dashboard/token)
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
|
||||
## Set Infisical service token in Gitlab
|
||||
To provide Infisical CLI with the service token generated in the previous step, go to **Settings > CI/CD > Variables** in Gitlab and create a new **INFISICAL_TOKEN** variable. Enter the generated service token as its value.
|
||||
<Tabs>
|
||||
<Tab title="Standard">
|
||||
## Navigate to your project's integrations tab
|
||||
|
||||
## Configure Infisical in your pipeline
|
||||
Edit your .gitlab-ci.yml to include the installation of the Infisical CLI. This will allow you to use the CLI for fetching and injecting secrets into any script or command within your Gitlab CI/CD process.
|
||||

|
||||
|
||||
## Authorize Infisical for GitLab
|
||||
|
||||
Press on the GitLab tile and grant Infisical access to your GitLab account.
|
||||
|
||||

|
||||
|
||||
<Info>
|
||||
If this is your project's first cloud integration, then you'll have to grant
|
||||
Infisical access to your project's environment variables. Although this step
|
||||
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||
the cloud platform.
|
||||
</Info>
|
||||
## Start integration
|
||||
|
||||
Select which Infisical environment secrets you want to sync to which GitLab repository and press create integration to start syncing secrets to GitLab.
|
||||
|
||||

|
||||

|
||||
|
||||
</Tab>
|
||||
<Tab title="Pipeline">
|
||||
|
||||
## Generate service token
|
||||
|
||||
Generate an [Infisical Token](../../getting-started/dashboard/token) for the specific project and environment in Infisical.
|
||||
|
||||
## Set the Infisical Token in Gitlab
|
||||
|
||||
Create a new variable called `INFISICAL_TOKEN` with the value set to the token from the previous step in Settings > CI/CD > Variables of your GitLab repository.
|
||||
|
||||
## Configure Infisical in your pipeline
|
||||
|
||||
Edit your `.gitlab-ci.yml` to include the Infisical CLI installation. This will allow you to use the CLI for fetching and injecting secrets into any script or command within your Gitlab CI/CD process.
|
||||
|
||||
#### Example
|
||||
```yaml
|
||||
|
||||
```yaml
|
||||
image: ubuntu
|
||||
|
||||
stages:
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
|
||||
build-job:
|
||||
build-job:
|
||||
stage: build
|
||||
script:
|
||||
- apt update && apt install -y curl
|
||||
- curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash
|
||||
- apt-get update && apt-get install -y infisical
|
||||
- infisical run -- npm run build
|
||||
```
|
||||
|
||||
...
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
36
docs/integrations/cloud/azure-key-vault.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: "Azure Key Vault"
|
||||
description: "How to automatically sync secrets from Infisical into your Azure Key Vault."
|
||||
---
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
- Set up Azure and have an existing key vault
|
||||
|
||||
## Navigate to your project's integrations tab
|
||||
|
||||

|
||||
|
||||
## Authorize Infisical for Azure Key Vault
|
||||
|
||||
Press on the Azure Key Vault tile and grant Infisical access to Azure Key Vault.
|
||||
|
||||
## Start Integration
|
||||
|
||||
Obtain the Vault URI of your key vault in the Overview tab.
|
||||
|
||||

|
||||
|
||||
Select which Infisical environment secrets you want to sync to your key vault. Then, input your Vault URI from the previous step. Finally, press create integration to start syncing secrets to Azure Key Vault.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
<Info>
|
||||
If this is your project's first cloud integration, then you'll have to grant
|
||||
Infisical access to your project's environment variables. Although this step
|
||||
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||
the cloud platform.
|
||||
</Info>
|
34
docs/integrations/frameworks/sveltekit.mdx
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: "SvelteKit"
|
||||
description: "How to use Infisical to inject environment variables and secrets into a SvelteKit app."
|
||||
---
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
- [Install the CLI](/cli/overview)
|
||||
|
||||
## Initialize Infisical for your [SvelteKit](https://kit.svelte.dev) app
|
||||
|
||||
```bash
|
||||
# navigate to the root of your of your project
|
||||
cd /path/to/project
|
||||
|
||||
# then initialize infisical
|
||||
infisical init
|
||||
```
|
||||
|
||||
## Start your application as usual but with Infisical
|
||||
|
||||
```bash
|
||||
infisical run -- <your application start command>
|
||||
|
||||
# Example
|
||||
infisical run -- npm run dev
|
||||
```
|
||||
|
||||
<Note>
|
||||
Note that for environment variables to be exposed to the client, you'll have
|
||||
to prefix them with `PUBLIC_`. Read more about that
|
||||
[here](https://kit.svelte.dev/docs/modules#$env-static-public).
|
||||
</Note>
|
34
docs/integrations/frameworks/terraform.mdx
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: "Terraform"
|
||||
description: "How to use Infisical to inject environment variables and secrets into terraform."
|
||||
---
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
- [Install the CLI](/cli/overview)
|
||||
|
||||
## Initialize Infisical for your [Terraform](https://www.terraform.io/) project
|
||||
|
||||
```bash
|
||||
# navigate to the root of your of your project
|
||||
cd /path/to/project
|
||||
|
||||
# then initialize Infisical
|
||||
infisical init
|
||||
```
|
||||
|
||||
## Run terraform as usual but with Infisical
|
||||
|
||||
```bash
|
||||
infisical run -- <your application start command>
|
||||
|
||||
# Example
|
||||
infisical run -- terraform plan
|
||||
```
|
||||
|
||||
<Note>
|
||||
To inject any arbitrary variable to terraform, you have
|
||||
to prefix them with `TF_VAR`. Read more about that
|
||||
[here](https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_var_name).
|
||||
</Note>
|
@@ -12,6 +12,7 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
|
||||
| [Docker](/integrations/platforms/docker) | Platform | Available |
|
||||
| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available |
|
||||
| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available |
|
||||
| [Terraform](/integrations/frameworks/terraform) | Infrastructure as code | Available |
|
||||
| [PM2](/integrations/platforms/pm2) | Platform | Available |
|
||||
| [Heroku](/integrations/cloud/heroku) | Cloud | Available |
|
||||
| [Vercel](/integrations/cloud/vercel) | Cloud | Available |
|
||||
@@ -20,8 +21,9 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
|
||||
| [Fly.io](/integrations/cloud/flyio) | Cloud | Available |
|
||||
| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available |
|
||||
| [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available |
|
||||
| [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available |
|
||||
| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available |
|
||||
| [GitLab Pipeline](/integrations/cicd/gitlab) | CI/CD | Available |
|
||||
| [GitLab](/integrations/cicd/gitlab) | CI/CD | Available |
|
||||
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available |
|
||||
| [Travis CI](/integrations/cicd/travisci) | CI/CD | Available |
|
||||
| [React](/integrations/frameworks/react) | Framework | Available |
|
||||
@@ -29,6 +31,7 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
|
||||
| [Express](/integrations/frameworks/express) | Framework | Available |
|
||||
| [Next.js](/integrations/frameworks/nextjs) | Framework | Available |
|
||||
| [NestJS](/integrations/frameworks/nestjs) | Framework | Available |
|
||||
| [SvelteKit](/integrations/frameworks/sveltekit) | Framework | Available |
|
||||
| [Nuxt](/integrations/frameworks/nuxt) | Framework | Available |
|
||||
| [Gatsby](/integrations/frameworks/gatsby) | Framework | Available |
|
||||
| [Remix](/integrations/frameworks/remix) | Framework | Available |
|
||||
@@ -38,9 +41,7 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
|
||||
| [Flask](/integrations/frameworks/flask) | Framework | Available |
|
||||
| [Laravel](/integrations/frameworks/laravel) | Framework | Available |
|
||||
| [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available |
|
||||
| AWS | Cloud | Coming soon |
|
||||
| GCP | Cloud | Coming soon |
|
||||
| Azure | Cloud | Coming soon |
|
||||
| DigitalOcean | Cloud | Coming soon |
|
||||
| GitHub Actions | CI/CD | Coming soon |
|
||||
| Jenkins | CI/CD | Coming soon |
|
||||
|
@@ -42,7 +42,7 @@ Starting your service with the Infisical CLI pulls your secrets from Infisical a
|
||||
CMD ["infisical", "run", "---", "[your service start command]"]
|
||||
|
||||
# example with single single command
|
||||
CMD ["infisical", "run", "---", "npm run start"]
|
||||
CMD ["infisical", "run", "---", "npm", "run", "start"]
|
||||
|
||||
# example with multiple commands
|
||||
CMD ["infisical", "run", "--command", "npm run start && ..."]
|
||||
|
@@ -50,7 +50,7 @@ metadata:
|
||||
# Name of of this InfisicalSecret resource
|
||||
name: infisicalsecret-sample
|
||||
spec:
|
||||
# The host that should be used to pull secrets from. The default value is https://app.infisical.com/api.
|
||||
# The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used
|
||||
hostAPI: https://app.infisical.com/api
|
||||
|
||||
# The Kubernetes secret the stores the Infisical token
|
||||
@@ -288,8 +288,43 @@ spec:
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
## Global configuration
|
||||
To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap.
|
||||
For example, you can configure all `InfisicalSecret` instances to fetch secrets from a single backend API without specifying the `hostAPI` parameter for each instance.
|
||||
|
||||
## Troubleshoot
|
||||
### Available global properties
|
||||
| Property | Description | Default value
|
||||
| -------- | ------------------------------------- |------------------------
|
||||
| hostAPI | If `hostAPI` in `InfisicalSecret` instance is left empty, this value will be used | https://app.infisical.com/api
|
||||
|
||||
|
||||
### Applying global configurations
|
||||
All global configurations must reside in a Kubernetes ConfigMap named `infisical-config` in the namespace `infisical-operator-system`.
|
||||
To apply global configuration to the operator, copy the following yaml into `infisical-config.yaml` file.
|
||||
|
||||
```yaml infisical-config.yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: infisical-operator-system
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: infisical-config
|
||||
namespace: infisical-operator-system
|
||||
data:
|
||||
hostAPI: https://example.com/api # <-- global hostAPI
|
||||
```
|
||||
|
||||
Then apply this change via kubectl by running the following
|
||||
|
||||
```bash
|
||||
kubectl apply -f infisical-config.yaml
|
||||
```
|
||||
|
||||
|
||||
## Troubleshoot operator
|
||||
|
||||
If the operator is unable to fetch secrets from the API, it will not affect the managed Kubernetes secret.
|
||||
It will continue attempting to reconnect to the API indefinitely.
|
||||
|
123
docs/mint.json
@@ -54,11 +54,6 @@
|
||||
"icon": "cloud",
|
||||
"url": "api-reference"
|
||||
},
|
||||
{
|
||||
"name": "Integrations",
|
||||
"icon": "plug",
|
||||
"url": "integrations"
|
||||
},
|
||||
{
|
||||
"name": "Contributing",
|
||||
"icon": "code",
|
||||
@@ -94,7 +89,6 @@
|
||||
"pages": [
|
||||
"getting-started/dashboard/organization",
|
||||
"getting-started/dashboard/project",
|
||||
"getting-started/dashboard/integrations",
|
||||
"getting-started/dashboard/pit-recovery",
|
||||
"getting-started/dashboard/secret-versioning",
|
||||
"getting-started/dashboard/audit-logs",
|
||||
@@ -103,7 +97,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "CLI",
|
||||
"group": "Command line",
|
||||
"pages": [
|
||||
"cli/overview",
|
||||
"cli/usage",
|
||||
@@ -123,6 +117,61 @@
|
||||
"cli/faq"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Integrations",
|
||||
"pages": [
|
||||
"integrations/overview",
|
||||
{
|
||||
"group": "Docker",
|
||||
"pages": [
|
||||
"integrations/platforms/docker",
|
||||
"integrations/platforms/docker-compose"
|
||||
]
|
||||
},
|
||||
|
||||
"integrations/platforms/kubernetes",
|
||||
"integrations/frameworks/terraform",
|
||||
|
||||
{
|
||||
"group": "AWS",
|
||||
"pages": [
|
||||
"integrations/cloud/aws-parameter-store",
|
||||
"integrations/cloud/aws-secret-manager"
|
||||
]
|
||||
},
|
||||
|
||||
"integrations/cloud/heroku",
|
||||
"integrations/cloud/vercel",
|
||||
"integrations/cloud/netlify",
|
||||
"integrations/cloud/render",
|
||||
"integrations/cloud/flyio",
|
||||
|
||||
"integrations/cloud/azure-key-vault",
|
||||
|
||||
"integrations/cicd/githubactions",
|
||||
"integrations/cicd/gitlab",
|
||||
"integrations/cicd/circleci",
|
||||
"integrations/cicd/travisci",
|
||||
|
||||
"integrations/frameworks/react",
|
||||
"integrations/frameworks/vue",
|
||||
"integrations/frameworks/express",
|
||||
"integrations/frameworks/nextjs",
|
||||
"integrations/frameworks/nestjs",
|
||||
"integrations/frameworks/sveltekit",
|
||||
"integrations/frameworks/nuxt",
|
||||
"integrations/frameworks/gatsby",
|
||||
"integrations/frameworks/remix",
|
||||
"integrations/frameworks/vite",
|
||||
"integrations/frameworks/fiber",
|
||||
"integrations/frameworks/django",
|
||||
"integrations/frameworks/flask",
|
||||
"integrations/frameworks/laravel",
|
||||
"integrations/frameworks/rails",
|
||||
"integrations/frameworks/dotnet",
|
||||
"integrations/platforms/pm2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Self-hosting",
|
||||
"pages": [
|
||||
@@ -200,63 +249,15 @@
|
||||
"api-reference/endpoints/secrets/versions",
|
||||
"api-reference/endpoints/secrets/rollback-version"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Service Tokens",
|
||||
"pages": [
|
||||
"api-reference/endpoints/service-tokens/get"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Integrations",
|
||||
"pages": ["integrations/overview"]
|
||||
},
|
||||
{
|
||||
"group": "Platforms",
|
||||
"pages": [
|
||||
"integrations/platforms/docker",
|
||||
"integrations/platforms/docker-compose",
|
||||
"integrations/platforms/kubernetes",
|
||||
"integrations/platforms/pm2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Cloud",
|
||||
"pages": [
|
||||
"integrations/cloud/heroku",
|
||||
"integrations/cloud/vercel",
|
||||
"integrations/cloud/netlify",
|
||||
"integrations/cloud/render",
|
||||
"integrations/cloud/flyio",
|
||||
"integrations/cloud/aws-parameter-store",
|
||||
"integrations/cloud/aws-secret-manager"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "CI/CD",
|
||||
"pages": [
|
||||
"integrations/cicd/githubactions",
|
||||
"integrations/cicd/gitlab",
|
||||
"integrations/cicd/circleci",
|
||||
"integrations/cicd/travisci"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Frameworks",
|
||||
"pages": [
|
||||
"integrations/frameworks/react",
|
||||
"integrations/frameworks/vue",
|
||||
"integrations/frameworks/express",
|
||||
"integrations/frameworks/nextjs",
|
||||
"integrations/frameworks/nestjs",
|
||||
"integrations/frameworks/nuxt",
|
||||
"integrations/frameworks/gatsby",
|
||||
"integrations/frameworks/remix",
|
||||
"integrations/frameworks/vite",
|
||||
"integrations/frameworks/fiber",
|
||||
"integrations/frameworks/django",
|
||||
"integrations/frameworks/flask",
|
||||
"integrations/frameworks/laravel",
|
||||
"integrations/frameworks/rails",
|
||||
"integrations/frameworks/dotnet"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Security",
|
||||
"pages": [
|
||||
|
@@ -134,4 +134,33 @@ SMTP_FROM_NAME=Infisical
|
||||
Remember that you will need to restart Infisical for this to work properly.
|
||||
</Info>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Zoho Mail">
|
||||
|
||||
1. Create an account and configure [Zoho Mail](https://www.zoho.com/mail/) to send emails.
|
||||
|
||||
2. With your email credentials, you can now set up your SMTP environment variables:
|
||||
|
||||
```
|
||||
SMTP_HOST=smtp.zoho.com
|
||||
SMTP_USERNAME=username # your email
|
||||
SMTP_PASSWORD=password # your password
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=true
|
||||
SMTP_FROM_ADDRESS=hey@example.com # your personal Zoho email or domain-based email linked to Zoho Mail
|
||||
SMTP_FROM_NAME=Infisical
|
||||
```
|
||||
|
||||
<Note>
|
||||
You can use either your personal Zoho email address like `you@zohomail.com` or
|
||||
a domain-based email address like `you@yourdomain.com`. If using a
|
||||
domain-based email address, then please make sure that you've configured and
|
||||
verified it with Zoho Mail.
|
||||
</Note>
|
||||
|
||||
<Info>
|
||||
Remember that you will need to restart Infisical for this to work properly.
|
||||
</Info>
|
||||
</Accordion>
|
||||
|
||||
</AccordionGroup>
|
||||
|
@@ -33,8 +33,7 @@ wget -O .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.exa
|
||||
wget -O docker-compose.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.yml
|
||||
|
||||
# Download nginx config
|
||||
mkdir nginx && cd nginx && wget -O default.conf https://raw.githubusercontent.com/Infisical/infisical/main/nginx/default.dev.conf
|
||||
cd ..
|
||||
mkdir nginx && wget -O ./nginx/default.conf https://raw.githubusercontent.com/Infisical/infisical/main/nginx/default.dev.conf
|
||||
```
|
||||
|
||||
3. Tweak the `.env` according to your preferences. Refer to the available [environment variables](../../self-hosting/configuration/envars)
|
||||
@@ -51,4 +50,4 @@ nano .env
|
||||
docker-compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
5. Your Infisical installation is complete and should be running on [http://localhost:8080](http://localhost:8080). Please note that the containers are not exposed to the internet and only bind to the localhost. It's up to you to configure a firewall, SSL certificates, and implement any additional security measures.
|
||||
5. Your Infisical installation is complete and should be running on [http://localhost:80](http://localhost:80). Please note that the containers are not exposed to the internet and only bind to the localhost. It's up to you to configure a firewall, SSL certificates, and implement any additional security measures.
|
||||
|
589
docs/spec.yaml
@@ -309,78 +309,6 @@ paths:
|
||||
example: any
|
||||
code:
|
||||
example: any
|
||||
/api/v1/signup/complete-account/signup:
|
||||
post:
|
||||
description: ''
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
'403':
|
||||
description: Forbidden
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
example: any
|
||||
firstName:
|
||||
example: any
|
||||
lastName:
|
||||
example: any
|
||||
publicKey:
|
||||
example: any
|
||||
encryptedPrivateKey:
|
||||
example: any
|
||||
iv:
|
||||
example: any
|
||||
tag:
|
||||
example: any
|
||||
salt:
|
||||
example: any
|
||||
verifier:
|
||||
example: any
|
||||
organizationName:
|
||||
example: any
|
||||
/api/v1/signup/complete-account/invite:
|
||||
post:
|
||||
description: ''
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
'403':
|
||||
description: Forbidden
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
example: any
|
||||
firstName:
|
||||
example: any
|
||||
lastName:
|
||||
example: any
|
||||
publicKey:
|
||||
example: any
|
||||
encryptedPrivateKey:
|
||||
example: any
|
||||
iv:
|
||||
example: any
|
||||
tag:
|
||||
example: any
|
||||
salt:
|
||||
example: any
|
||||
verifier:
|
||||
example: any
|
||||
/api/v1/auth/token:
|
||||
post:
|
||||
description: ''
|
||||
@@ -412,7 +340,11 @@ paths:
|
||||
/api/v1/auth/login2:
|
||||
post:
|
||||
description: ''
|
||||
parameters: []
|
||||
parameters:
|
||||
- name: user-agent
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
@@ -431,7 +363,11 @@ paths:
|
||||
/api/v1/auth/logout:
|
||||
post:
|
||||
description: ''
|
||||
parameters: []
|
||||
parameters:
|
||||
- name: user-agent
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
@@ -691,6 +627,18 @@ paths:
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
/api/v1/organization/{organizationId}/workspace-memberships:
|
||||
get:
|
||||
description: ''
|
||||
parameters:
|
||||
- name: organizationId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
/api/v1/workspace/{workspaceId}/keys:
|
||||
get:
|
||||
description: ''
|
||||
@@ -933,6 +881,26 @@ paths:
|
||||
properties:
|
||||
role:
|
||||
example: any
|
||||
/api/v1/membership/{membershipId}/deny-permissions:
|
||||
post:
|
||||
description: ''
|
||||
parameters:
|
||||
- name: membershipId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
permissions:
|
||||
example: any
|
||||
/api/v1/key/{workspaceId}:
|
||||
post:
|
||||
description: ''
|
||||
@@ -1147,11 +1115,17 @@ paths:
|
||||
properties:
|
||||
clientProof:
|
||||
example: any
|
||||
protectedKey:
|
||||
example: any
|
||||
protectedKeyIV:
|
||||
example: any
|
||||
protectedKeyTag:
|
||||
example: any
|
||||
encryptedPrivateKey:
|
||||
example: any
|
||||
iv:
|
||||
encryptedPrivateKeyIV:
|
||||
example: any
|
||||
tag:
|
||||
encryptedPrivateKeyTag:
|
||||
example: any
|
||||
salt:
|
||||
example: any
|
||||
@@ -1247,11 +1221,17 @@ paths:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
protectedKey:
|
||||
example: any
|
||||
protectedKeyIV:
|
||||
example: any
|
||||
protectedKeyTag:
|
||||
example: any
|
||||
encryptedPrivateKey:
|
||||
example: any
|
||||
iv:
|
||||
encryptedPrivateKeyIV:
|
||||
example: any
|
||||
tag:
|
||||
encryptedPrivateKeyTag:
|
||||
example: any
|
||||
salt:
|
||||
example: any
|
||||
@@ -1270,6 +1250,39 @@ paths:
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
/api/v1/integration/:
|
||||
post:
|
||||
description: ''
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
integrationAuthId:
|
||||
example: any
|
||||
app:
|
||||
example: any
|
||||
appId:
|
||||
example: any
|
||||
isActive:
|
||||
example: any
|
||||
sourceEnvironment:
|
||||
example: any
|
||||
targetEnvironment:
|
||||
example: any
|
||||
owner:
|
||||
example: any
|
||||
path:
|
||||
example: any
|
||||
region:
|
||||
example: any
|
||||
/api/v1/integration/{integrationId}:
|
||||
patch:
|
||||
description: ''
|
||||
@@ -1290,17 +1303,17 @@ paths:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
app:
|
||||
example: any
|
||||
environment:
|
||||
example: any
|
||||
isActive:
|
||||
example: any
|
||||
target:
|
||||
app:
|
||||
example: any
|
||||
context:
|
||||
appId:
|
||||
example: any
|
||||
siteId:
|
||||
targetEnvironment:
|
||||
example: any
|
||||
owner:
|
||||
example: any
|
||||
delete:
|
||||
description: ''
|
||||
@@ -1322,6 +1335,33 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
/api/v1/integration-auth/{integrationAuthId}:
|
||||
get:
|
||||
description: ''
|
||||
parameters:
|
||||
- name: integrationAuthId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
delete:
|
||||
description: ''
|
||||
parameters:
|
||||
- name: integrationAuthId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
/api/v1/integration-auth/oauth-token:
|
||||
post:
|
||||
description: ''
|
||||
@@ -1343,6 +1383,29 @@ paths:
|
||||
example: any
|
||||
integration:
|
||||
example: any
|
||||
/api/v1/integration-auth/access-token:
|
||||
post:
|
||||
description: ''
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
workspaceId:
|
||||
example: any
|
||||
accessId:
|
||||
example: any
|
||||
accessToken:
|
||||
example: any
|
||||
integration:
|
||||
example: any
|
||||
/api/v1/integration-auth/{integrationAuthId}/apps:
|
||||
get:
|
||||
description: ''
|
||||
@@ -1357,13 +1420,115 @@ paths:
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
/api/v1/integration-auth/{integrationAuthId}:
|
||||
delete:
|
||||
/api/v2/signup/complete-account/signup:
|
||||
post:
|
||||
description: ''
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
'403':
|
||||
description: Forbidden
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
example: any
|
||||
firstName:
|
||||
example: any
|
||||
lastName:
|
||||
example: any
|
||||
protectedKey:
|
||||
example: any
|
||||
protectedKeyIV:
|
||||
example: any
|
||||
protectedKeyTag:
|
||||
example: any
|
||||
publicKey:
|
||||
example: any
|
||||
encryptedPrivateKey:
|
||||
example: any
|
||||
encryptedPrivateKeyIV:
|
||||
example: any
|
||||
encryptedPrivateKeyTag:
|
||||
example: any
|
||||
salt:
|
||||
example: any
|
||||
verifier:
|
||||
example: any
|
||||
organizationName:
|
||||
example: any
|
||||
/api/v2/signup/complete-account/invite:
|
||||
post:
|
||||
description: ''
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
'403':
|
||||
description: Forbidden
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
example: any
|
||||
firstName:
|
||||
example: any
|
||||
lastName:
|
||||
example: any
|
||||
protectedKey:
|
||||
example: any
|
||||
protectedKeyIV:
|
||||
example: any
|
||||
protectedKeyTag:
|
||||
example: any
|
||||
publicKey:
|
||||
example: any
|
||||
encryptedPrivateKey:
|
||||
example: any
|
||||
encryptedPrivateKeyIV:
|
||||
example: any
|
||||
encryptedPrivateKeyTag:
|
||||
example: any
|
||||
salt:
|
||||
example: any
|
||||
verifier:
|
||||
example: any
|
||||
/api/v2/auth/login1:
|
||||
post:
|
||||
description: ''
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
example: any
|
||||
clientPublicKey:
|
||||
example: any
|
||||
/api/v2/auth/login2:
|
||||
post:
|
||||
description: ''
|
||||
parameters:
|
||||
- name: integrationAuthId
|
||||
in: path
|
||||
required: true
|
||||
- name: user-agent
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
@@ -1371,6 +1536,54 @@ paths:
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
example: any
|
||||
clientProof:
|
||||
example: any
|
||||
/api/v2/auth/mfa/send:
|
||||
post:
|
||||
description: ''
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
example: any
|
||||
/api/v2/auth/mfa/verify:
|
||||
post:
|
||||
description: ''
|
||||
parameters:
|
||||
- name: user-agent
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
example: any
|
||||
mfaToken:
|
||||
example: any
|
||||
/api/v2/users/me:
|
||||
get:
|
||||
summary: Retrieve the current user on the request
|
||||
@@ -1392,6 +1605,23 @@ paths:
|
||||
description: Bad Request
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
/api/v2/users/me/mfa:
|
||||
patch:
|
||||
description: ''
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
isMfaEnabled:
|
||||
example: any
|
||||
/api/v2/users/me/organizations:
|
||||
get:
|
||||
summary: Return organizations that current user is part of
|
||||
@@ -1615,6 +1845,62 @@ paths:
|
||||
properties:
|
||||
environmentSlug:
|
||||
example: any
|
||||
get:
|
||||
description: ''
|
||||
parameters:
|
||||
- name: workspaceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
/api/v2/workspace/{workspaceId}/tags:
|
||||
get:
|
||||
description: ''
|
||||
parameters:
|
||||
- name: workspaceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
post:
|
||||
description: ''
|
||||
parameters:
|
||||
- name: workspaceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
example: any
|
||||
slug:
|
||||
example: any
|
||||
/api/v2/workspace/tags/{tagId}:
|
||||
delete:
|
||||
description: ''
|
||||
parameters:
|
||||
- name: tagId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
/api/v2/workspace/{workspaceId}/secrets:
|
||||
post:
|
||||
description: ''
|
||||
@@ -1804,6 +2090,28 @@ paths:
|
||||
description: Bad Request
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
/api/v2/workspace/{workspaceId}/auto-capitalization:
|
||||
patch:
|
||||
description: ''
|
||||
parameters:
|
||||
- name: workspaceId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
autoCapitalization:
|
||||
example: any
|
||||
/api/v2/secret/batch-create/workspace/{workspaceId}/environment/{environment}:
|
||||
post:
|
||||
description: ''
|
||||
@@ -1968,11 +2276,38 @@ paths:
|
||||
properties:
|
||||
secret:
|
||||
example: any
|
||||
/api/v2/secrets/batch:
|
||||
post:
|
||||
description: ''
|
||||
parameters:
|
||||
- name: user-agent
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
workspaceId:
|
||||
example: any
|
||||
environment:
|
||||
example: any
|
||||
requests:
|
||||
example: any
|
||||
/api/v2/secrets/:
|
||||
post:
|
||||
summary: Create new secret(s)
|
||||
description: Create one or many secrets for a given project and environment.
|
||||
parameters: []
|
||||
parameters:
|
||||
- name: user-agent
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
@@ -2022,6 +2357,10 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: user-agent
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
- name: content
|
||||
in: query
|
||||
schema:
|
||||
@@ -2073,7 +2412,11 @@ paths:
|
||||
delete:
|
||||
summary: Delete secret(s)
|
||||
description: Delete one or many secrets by their ID(s)
|
||||
parameters: []
|
||||
parameters:
|
||||
- name: user-agent
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
@@ -2101,13 +2444,23 @@ paths:
|
||||
description: ID(s) of secrets - string or array of strings
|
||||
/api/v2/service-token/:
|
||||
get:
|
||||
description: ''
|
||||
summary: Return Infisical Token data
|
||||
description: Return Infisical Token data
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'400':
|
||||
description: Bad Request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
serviceTokenData:
|
||||
type: object
|
||||
$ref: '#/components/schemas/ServiceTokenData'
|
||||
description: Details of service token
|
||||
security:
|
||||
- bearerAuth: []
|
||||
post:
|
||||
description: ''
|
||||
parameters: []
|
||||
@@ -2136,6 +2489,8 @@ paths:
|
||||
example: any
|
||||
expiresIn:
|
||||
example: any
|
||||
permissions:
|
||||
example: any
|
||||
/api/v2/service-token/{serviceTokenDataId}:
|
||||
delete:
|
||||
description: ''
|
||||
@@ -2317,9 +2672,6 @@ components:
|
||||
Project:
|
||||
type: object
|
||||
properties:
|
||||
_id:
|
||||
type: string
|
||||
example: ''
|
||||
name:
|
||||
type: string
|
||||
example: My Project
|
||||
@@ -2602,6 +2954,51 @@ components:
|
||||
secretValueTag:
|
||||
type: string
|
||||
example: ''
|
||||
ServiceTokenData:
|
||||
type: object
|
||||
properties:
|
||||
_id:
|
||||
type: string
|
||||
example: ''
|
||||
name:
|
||||
type: string
|
||||
example: ''
|
||||
workspace:
|
||||
type: string
|
||||
example: ''
|
||||
environment:
|
||||
type: string
|
||||
example: ''
|
||||
user:
|
||||
type: object
|
||||
properties:
|
||||
_id:
|
||||
type: string
|
||||
example: ''
|
||||
firstName:
|
||||
type: string
|
||||
example: ''
|
||||
lastName:
|
||||
type: string
|
||||
example: ''
|
||||
expiresAt:
|
||||
type: string
|
||||
example: '2023-01-13T14:16:12.210Z'
|
||||
encryptedKey:
|
||||
type: string
|
||||
example: ''
|
||||
iv:
|
||||
type: string
|
||||
example: ''
|
||||
tag:
|
||||
type: string
|
||||
example: ''
|
||||
updatedAt:
|
||||
type: string
|
||||
example: '2023-01-13T14:16:12.210Z'
|
||||
createdAt:
|
||||
type: string
|
||||
example: '2023-01-13T14:16:12.210Z'
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
|
@@ -8,7 +8,7 @@ module.exports = {
|
||||
debug: false,
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: ["en", "ko", "fr", "pt-BR", "pt-PT"],
|
||||
locales: ["en", "ko", "fr", "pt-BR", "pt-PT", "es"],
|
||||
},
|
||||
fallbackLng: {
|
||||
default: ["en"],
|
||||
@@ -23,4 +23,4 @@ module.exports = {
|
||||
// strictMode: true,
|
||||
// serializeConfig: false,
|
||||
// react: { useSuspense: false }
|
||||
};
|
||||
};
|
122
frontend/package-lock.json
generated
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"name": "npm-proj-1677883018530-0.7603125731052582NtcmfK",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
@@ -13,7 +13,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.19",
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@headlessui/react": "^1.7.7",
|
||||
"@hookform/resolvers": "^2.9.10",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
"@radix-ui/react-accordion": "^1.1.0",
|
||||
@@ -30,14 +30,14 @@
|
||||
"@radix-ui/react-tabs": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.2",
|
||||
"@reduxjs/toolkit": "^1.8.3",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/react-stripe-js": "^1.16.3",
|
||||
"@stripe/stripe-js": "^1.46.0",
|
||||
"@tanstack/react-query": "^4.23.0",
|
||||
"@types/argon2-browser": "^1.18.1",
|
||||
"add": "^2.0.6",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"axios": "^0.27.2",
|
||||
"axios-auth-refresh": "^3.3.3",
|
||||
"axios-auth-refresh": "^3.3.6",
|
||||
"base64-loader": "^1.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"cookies": "^0.8.0",
|
||||
@@ -45,13 +45,13 @@
|
||||
"fs": "^0.0.1-security",
|
||||
"gray-matter": "^4.0.3",
|
||||
"http-proxy": "^1.18.1",
|
||||
"i18next": "^22.4.6",
|
||||
"i18next": "^22.4.9",
|
||||
"jspdf": "^2.5.1",
|
||||
"jsrp": "^0.2.4",
|
||||
"markdown-it": "^13.0.1",
|
||||
"next": "^12.3.4",
|
||||
"next-i18next": "^13.0.2",
|
||||
"posthog-js": "^1.34.0",
|
||||
"posthog-js": "^1.39.4",
|
||||
"query-string": "^7.1.3",
|
||||
"react": "^17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
@@ -66,7 +66,7 @@
|
||||
"react-table": "^7.8.0",
|
||||
"set-cookie-parser": "^2.5.1",
|
||||
"sharp": "^0.31.2",
|
||||
"styled-components": "^5.3.5",
|
||||
"styled-components": "^5.3.7",
|
||||
"tailwind-merge": "^1.8.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
@@ -2544,9 +2544,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.6.6.tgz",
|
||||
"integrity": "sha512-MFJtmj9Xh/hhBMhLccGbBoSk+sk61BlP6sJe4uQcVMtXZhCgGqd2GyIQzzmsdPdTEWGSF434CBi8mnhR6um46Q==",
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.7.tgz",
|
||||
"integrity": "sha512-BqDOd/tB9u2tA0T3Z0fn18ktw+KbVwMnkxxsGPIH2hzssrQhKB5n/6StZOyvLYP/FsYtvuXfi9I0YowKPv2c1w==",
|
||||
"dependencies": {
|
||||
"client-only": "^0.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -3935,9 +3938,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@sentry/types": {
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.15.0.tgz",
|
||||
"integrity": "sha512-MN9haDRh9ZOsTotoDTHu2BT3sT8Vs1F0alhizUpDyjN2YgBCqR6JV+AbAE1XNHwS2+5zbppch1PwJUVeE58URQ==",
|
||||
"version": "7.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.22.0.tgz",
|
||||
"integrity": "sha512-LhCL+wb1Jch+OesB2CIt6xpfO1Ab6CRvoNYRRzVumWPLns1T3ZJkarYfhbLaOEIb38EIbPgREdxn2AJT560U4Q==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -6281,14 +6284,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@stripe/react-stripe-js": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.10.0.tgz",
|
||||
"integrity": "sha512-vuIjJUZJ3nyiaGa5z5iyMCzZfGGsgzOOjWjqknbbhkNsewyyginfeky9EZLSz9+iSAsgC9K6MeNOTLKVGcMycQ==",
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.16.3.tgz",
|
||||
"integrity": "sha512-gS6UDGEuM92K50pFB3o//EqqPxmaqpC8IrsBda4P4LxeULoO0pBFSHXJ5Ab6uA7G2lyO2bluvSLereh0OH9GrQ==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@stripe/stripe-js": "^1.34.0",
|
||||
"@stripe/stripe-js": "^1.44.1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
@@ -8157,9 +8160,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios-auth-refresh": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/axios-auth-refresh/-/axios-auth-refresh-3.3.3.tgz",
|
||||
"integrity": "sha512-2IbDhJ/h6ddNBBnnzn1VFK/qx17pE9aVqiafB8rx5LVHsJ1HtFpUGkbXY7PzTG+8P9HJWcyA3fNZl9BikSuilg==",
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/axios-auth-refresh/-/axios-auth-refresh-3.3.6.tgz",
|
||||
"integrity": "sha512-2CeBUce/SxIfFxow5/n8vApJ97yYF6qoV4gh1UrswT7aEOnlOdBLxxyhOI4IaxGs6BY0l8YujU2jlc4aCmK17Q==",
|
||||
"peerDependencies": {
|
||||
"axios": ">= 0.18 < 0.19.0 || >= 0.19.1"
|
||||
}
|
||||
@@ -9205,6 +9208,11 @@
|
||||
"@colors/colors": "1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
||||
@@ -13224,9 +13232,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "22.4.6",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.6.tgz",
|
||||
"integrity": "sha512-9Tm1ezxWyzV+306CIDMBbYBitC1jedQyYuuLtIv7oxjp2ohh8eyxP9xytIf+2bbQfhH784IQKPSYp+Zq9+YSbw==",
|
||||
"version": "22.4.9",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.9.tgz",
|
||||
"integrity": "sha512-8gWMmUz460KJDQp/ob3MNUX84cVuDRY9PLFPnV8d+Qezz/6dkjxwOaH70xjrCNDO+JrUL25iXfAIN9wUkInNZw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -17079,11 +17087,11 @@
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.34.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.34.0.tgz",
|
||||
"integrity": "sha512-HkRwwzdz31N5ykQIO3SIkSS8nwhdqqnuDZ/qltitX4FhxrV9/tSRavEXz0YLvioOXeNVmQWtsN3krKajErwkwg==",
|
||||
"version": "1.39.4",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.39.4.tgz",
|
||||
"integrity": "sha512-Elpf1gwyuObueXi89iH+9pP+WhpkiivP8Qwej4RzOLwSTa7Floaa4rgAw7rnCnX1PtRoJ3F0kqb6q9T+aZjRiA==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "^7.2.0",
|
||||
"@sentry/types": "7.22.0",
|
||||
"fflate": "^0.4.1",
|
||||
"rrweb-snapshot": "^1.1.14"
|
||||
}
|
||||
@@ -20237,10 +20245,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/styled-components": {
|
||||
"version": "5.3.5",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.5.tgz",
|
||||
"integrity": "sha512-ndETJ9RKaaL6q41B69WudeqLzOpY1A/ET/glXkNZ2T7dPjPqpPCXXQjDFYZWwNnE5co0wX+gTCqx9mfxTmSIPg==",
|
||||
"hasInstallScript": true,
|
||||
"version": "5.3.7",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.7.tgz",
|
||||
"integrity": "sha512-JL1b4A79OGqav4TxkrNsuuQfy6ZnrpyQx6hBDQ3Hd3JyuR2IQuVNBpF+FCEWFNZpN5hj+fhkaEVWteVJ18f0tw==",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/traverse": "^7.4.5",
|
||||
@@ -23946,10 +23953,12 @@
|
||||
}
|
||||
},
|
||||
"@headlessui/react": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.6.6.tgz",
|
||||
"integrity": "sha512-MFJtmj9Xh/hhBMhLccGbBoSk+sk61BlP6sJe4uQcVMtXZhCgGqd2GyIQzzmsdPdTEWGSF434CBi8mnhR6um46Q==",
|
||||
"requires": {}
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.7.tgz",
|
||||
"integrity": "sha512-BqDOd/tB9u2tA0T3Z0fn18ktw+KbVwMnkxxsGPIH2hzssrQhKB5n/6StZOyvLYP/FsYtvuXfi9I0YowKPv2c1w==",
|
||||
"requires": {
|
||||
"client-only": "^0.0.1"
|
||||
}
|
||||
},
|
||||
"@hookform/resolvers": {
|
||||
"version": "2.9.10",
|
||||
@@ -24987,9 +24996,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.15.0.tgz",
|
||||
"integrity": "sha512-MN9haDRh9ZOsTotoDTHu2BT3sT8Vs1F0alhizUpDyjN2YgBCqR6JV+AbAE1XNHwS2+5zbppch1PwJUVeE58URQ=="
|
||||
"version": "7.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.22.0.tgz",
|
||||
"integrity": "sha512-LhCL+wb1Jch+OesB2CIt6xpfO1Ab6CRvoNYRRzVumWPLns1T3ZJkarYfhbLaOEIb38EIbPgREdxn2AJT560U4Q=="
|
||||
},
|
||||
"@sinclair/typebox": {
|
||||
"version": "0.24.51",
|
||||
@@ -26659,9 +26668,9 @@
|
||||
}
|
||||
},
|
||||
"@stripe/react-stripe-js": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.10.0.tgz",
|
||||
"integrity": "sha512-vuIjJUZJ3nyiaGa5z5iyMCzZfGGsgzOOjWjqknbbhkNsewyyginfeky9EZLSz9+iSAsgC9K6MeNOTLKVGcMycQ==",
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.16.3.tgz",
|
||||
"integrity": "sha512-gS6UDGEuM92K50pFB3o//EqqPxmaqpC8IrsBda4P4LxeULoO0pBFSHXJ5Ab6uA7G2lyO2bluvSLereh0OH9GrQ==",
|
||||
"requires": {
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
@@ -28115,9 +28124,9 @@
|
||||
}
|
||||
},
|
||||
"axios-auth-refresh": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/axios-auth-refresh/-/axios-auth-refresh-3.3.3.tgz",
|
||||
"integrity": "sha512-2IbDhJ/h6ddNBBnnzn1VFK/qx17pE9aVqiafB8rx5LVHsJ1HtFpUGkbXY7PzTG+8P9HJWcyA3fNZl9BikSuilg==",
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/axios-auth-refresh/-/axios-auth-refresh-3.3.6.tgz",
|
||||
"integrity": "sha512-2CeBUce/SxIfFxow5/n8vApJ97yYF6qoV4gh1UrswT7aEOnlOdBLxxyhOI4IaxGs6BY0l8YujU2jlc4aCmK17Q==",
|
||||
"requires": {}
|
||||
},
|
||||
"axobject-query": {
|
||||
@@ -28888,6 +28897,11 @@
|
||||
"string-width": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
|
||||
},
|
||||
"cliui": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
||||
@@ -31990,9 +32004,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"i18next": {
|
||||
"version": "22.4.6",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.6.tgz",
|
||||
"integrity": "sha512-9Tm1ezxWyzV+306CIDMBbYBitC1jedQyYuuLtIv7oxjp2ohh8eyxP9xytIf+2bbQfhH784IQKPSYp+Zq9+YSbw==",
|
||||
"version": "22.4.9",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.9.tgz",
|
||||
"integrity": "sha512-8gWMmUz460KJDQp/ob3MNUX84cVuDRY9PLFPnV8d+Qezz/6dkjxwOaH70xjrCNDO+JrUL25iXfAIN9wUkInNZw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.20.6"
|
||||
}
|
||||
@@ -34690,11 +34704,11 @@
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||
},
|
||||
"posthog-js": {
|
||||
"version": "1.34.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.34.0.tgz",
|
||||
"integrity": "sha512-HkRwwzdz31N5ykQIO3SIkSS8nwhdqqnuDZ/qltitX4FhxrV9/tSRavEXz0YLvioOXeNVmQWtsN3krKajErwkwg==",
|
||||
"version": "1.39.4",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.39.4.tgz",
|
||||
"integrity": "sha512-Elpf1gwyuObueXi89iH+9pP+WhpkiivP8Qwej4RzOLwSTa7Floaa4rgAw7rnCnX1PtRoJ3F0kqb6q9T+aZjRiA==",
|
||||
"requires": {
|
||||
"@sentry/types": "^7.2.0",
|
||||
"@sentry/types": "7.22.0",
|
||||
"fflate": "^0.4.1",
|
||||
"rrweb-snapshot": "^1.1.14"
|
||||
}
|
||||
@@ -36986,9 +37000,9 @@
|
||||
}
|
||||
},
|
||||
"styled-components": {
|
||||
"version": "5.3.5",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.5.tgz",
|
||||
"integrity": "sha512-ndETJ9RKaaL6q41B69WudeqLzOpY1A/ET/glXkNZ2T7dPjPqpPCXXQjDFYZWwNnE5co0wX+gTCqx9mfxTmSIPg==",
|
||||
"version": "5.3.7",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.7.tgz",
|
||||
"integrity": "sha512-JL1b4A79OGqav4TxkrNsuuQfy6ZnrpyQx6hBDQ3Hd3JyuR2IQuVNBpF+FCEWFNZpN5hj+fhkaEVWteVJ18f0tw==",
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/traverse": "^7.4.5",
|
||||
|
@@ -20,7 +20,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.19",
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@headlessui/react": "^1.7.7",
|
||||
"@hookform/resolvers": "^2.9.10",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
"@radix-ui/react-accordion": "^1.1.0",
|
||||
@@ -37,14 +37,14 @@
|
||||
"@radix-ui/react-tabs": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.2",
|
||||
"@reduxjs/toolkit": "^1.8.3",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/react-stripe-js": "^1.16.3",
|
||||
"@stripe/stripe-js": "^1.46.0",
|
||||
"@types/argon2-browser": "^1.18.1",
|
||||
"@tanstack/react-query": "^4.23.0",
|
||||
"add": "^2.0.6",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"axios": "^0.27.2",
|
||||
"axios-auth-refresh": "^3.3.3",
|
||||
"axios-auth-refresh": "^3.3.6",
|
||||
"base64-loader": "^1.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"cookies": "^0.8.0",
|
||||
@@ -52,13 +52,13 @@
|
||||
"fs": "^0.0.1-security",
|
||||
"gray-matter": "^4.0.3",
|
||||
"http-proxy": "^1.18.1",
|
||||
"i18next": "^22.4.6",
|
||||
"i18next": "^22.4.9",
|
||||
"jspdf": "^2.5.1",
|
||||
"jsrp": "^0.2.4",
|
||||
"markdown-it": "^13.0.1",
|
||||
"next": "^12.3.4",
|
||||
"next-i18next": "^13.0.2",
|
||||
"posthog-js": "^1.34.0",
|
||||
"posthog-js": "^1.39.4",
|
||||
"query-string": "^7.1.3",
|
||||
"react": "^17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
@@ -73,7 +73,7 @@
|
||||
"react-table": "^7.8.0",
|
||||
"set-cookie-parser": "^2.5.1",
|
||||
"sharp": "^0.31.2",
|
||||
"styled-components": "^5.3.5",
|
||||
"styled-components": "^5.3.7",
|
||||
"tailwind-merge": "^1.8.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
|
@@ -10,6 +10,7 @@ const integrationSlugNameMapping: Mapping = {
|
||||
'vercel': 'Vercel',
|
||||
'netlify': 'Netlify',
|
||||
'github': 'GitHub',
|
||||
'gitlab': 'GitLab',
|
||||
'render': 'Render',
|
||||
'flyio': 'Fly.io',
|
||||
'circleci': 'CircleCI',
|
||||
|
BIN
frontend/public/images/integrations/GitLab.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
frontend/public/images/integrations/Kubernetes.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
frontend/public/images/integrations/SvelteKit.png
Normal file
After Width: | Height: | Size: 6.6 KiB |