Merge remote-tracking branch 'refs/remotes/origin/main' into circleci-integration-branch

This commit is contained in:
Aashish-Upadhyay-101
2023-02-11 17:55:59 +05:45
137 changed files with 5954 additions and 499 deletions

3308
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,18 @@
{
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.267.0",
"@godaddy/terminus": "^4.11.2",
"@sentry/node": "^7.21.1",
"@sentry/tracing": "^7.21.1",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
"axios": "^1.2.0",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
"aws-sdk": "^2.1311.0",
"axios": "^1.1.3",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
@@ -15,17 +22,26 @@
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"jsonwebtoken": "^8.5.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"mongoose": "^6.7.3",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.7.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.1.0",
"posthog-node": "^2.2.2",
"query-string": "^7.1.3",
"request-ip": "^3.3.0",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
"swagger-autogen": "^2.22.0",
"swagger-ui-express": "^4.6.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3"
"typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
},
"name": "infisical-api",
"version": "1.0.0",
@@ -102,47 +118,5 @@
"suiteNameTemplate": "{filepath}",
"classNameTemplate": "{classname}",
"titleTemplate": "{title}"
},
"dependencies": {
"@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
"axios": "^1.1.3",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"express-rate-limit": "^6.7.0",
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.7.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.2.2",
"query-string": "^7.1.3",
"request-ip": "^3.3.0",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
"swagger-autogen": "^2.22.0",
"swagger-ui-express": "^4.6.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
}
}

View File

@@ -81,26 +81,31 @@ export const oAuthExchange = async (
};
/**
* Save integration access token as part of integration [integration] for workspace with id [workspaceId]
* @param req
* @param res
* Save integration access token and (optionally) access id as part of integration
* [integration] for workspace with id [workspaceId]
* @param req
* @param res
*/
export const saveIntegrationAccessToken = async (
req: Request,
res: Response
) => {
// TODO: refactor
let integrationAuth;
try {
const {
workspaceId,
accessToken,
integration,
}: {
workspaceId: string;
accessToken: string;
integration: string;
} = req.body;
// TODO: refactor
// TODO: check if access token is valid for each integration
let integrationAuth;
try {
const {
workspaceId,
accessId,
accessToken,
integration
}: {
workspaceId: string;
accessId: string | null;
accessToken: string;
integration: string;
} = req.body;
const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId),
@@ -120,9 +125,10 @@ export const saveIntegrationAccessToken = async (
upsert: true
});
// encrypt and save integration access token
// encrypt and save integration access details
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId,
accessToken,
accessExpiresAt: undefined
});

View File

@@ -26,7 +26,9 @@ export const createIntegration = async (req: Request, res: Response) => {
isActive,
sourceEnvironment,
targetEnvironment,
owner
owner,
path,
region
} = req.body;
// TODO: validate [sourceEnvironment] and [targetEnvironment]
@@ -40,6 +42,8 @@ export const createIntegration = async (req: Request, res: Response) => {
appId,
targetEnvironment,
owner,
path,
region,
integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId)
}).save();

View File

@@ -452,7 +452,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueTag,
tags,
...((
secretCommentCiphertext &&
secretCommentCiphertext !== undefined &&
secretCommentIV &&
secretCommentTag
) ? {

View File

@@ -2,7 +2,7 @@ import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Membership,
Membership, Secret,
} from '../../models';
import Tag, { ITag } from '../../models/tag';
import { Builder } from "builder-pattern"
@@ -54,6 +54,12 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => {
const result = await Tag.findByIdAndDelete(tagId);
// remove the tag from secrets
await Secret.updateMany(
{ tags: { $in: [tagId] } },
{ $pull: { tags: tagId } }
);
res.json(result);
}

View File

@@ -94,6 +94,7 @@ const handleOAuthExchangeHelper = async ({
// set integration auth access token
await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt
});
@@ -138,7 +139,7 @@ const syncIntegrationsHelper = async ({
if (!integrationAuth) throw new Error('Failed to find integration auth');
// get integration auth access token
const accessToken = await getIntegrationAuthAccessHelper({
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth.toString()
});
@@ -147,7 +148,8 @@ const syncIntegrationsHelper = async ({
integration,
integrationAuth,
secrets,
accessToken
accessId: access.accessId,
accessToken: access.accessToken
});
}
} catch (err) {
@@ -203,12 +205,12 @@ const syncIntegrationsHelper = async ({
* @returns {String} accessToken - decrypted access token
*/
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
let accessId;
let accessToken;
try {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext');
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
@@ -232,6 +234,15 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
});
}
}
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
accessId = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
ciphertext: integrationAuth.accessIdCiphertext as string,
iv: integrationAuth.accessIdIV as string,
tag: integrationAuth.accessIdTag as string
});
}
} catch (err) {
Sentry.setUser(null);
@@ -242,7 +253,10 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
throw new Error('Failed to get integration access token');
}
return accessToken;
return ({
accessId,
accessToken
});
}
/**
@@ -292,9 +306,9 @@ const setIntegrationAuthRefreshHelper = async ({
}
/**
* Encrypt access token [accessToken] using the bot's copy
* of the workspace key for workspace belonging to integration auth
* with id [integrationAuthId] and store it along with [accessExpiresAt]
* Encrypt access token [accessToken] and (optionally) access id [accessId]
* using the bot's copy of the workspace key for workspace belonging to
* integration auth with id [integrationAuthId] and store it along with [accessExpiresAt]
* @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.accessToken - access token
@@ -302,10 +316,12 @@ const setIntegrationAuthRefreshHelper = async ({
*/
const setIntegrationAuthAccessHelper = async ({
integrationAuthId,
accessId,
accessToken,
accessExpiresAt
}: {
integrationAuthId: string;
accessId: string | null;
accessToken: string;
accessExpiresAt: Date | undefined;
}) => {
@@ -315,17 +331,28 @@ const setIntegrationAuthAccessHelper = async ({
if (!integrationAuth) throw new Error('Failed to find integration auth');
const obj = await BotService.encryptSymmetric({
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
plaintext: accessToken
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
plaintext: accessId
});
}
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
accessCiphertext: obj.ciphertext,
accessIV: obj.iv,
accessTag: obj.tag,
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt
}, {
new: true

View File

@@ -4,6 +4,8 @@ import { Octokit } from "@octokit/rest";
import { IIntegrationAuth } from "../models";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -44,10 +46,14 @@ const getApps = async ({
try {
switch (integrationAuth.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
apps = await getAppsAzureKeyVault({
accessToken
});
break;
apps = [];
break;
case INTEGRATION_AWS_PARAMETER_STORE:
apps = [];
break;
case INTEGRATION_AWS_SECRET_MANAGER:
apps = [];
break;
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken,
@@ -94,15 +100,6 @@ const getApps = async ({
return apps;
};
const getAppsAzureKeyVault = async ({
accessToken
}: {
accessToken: string;
}) => {
// TODO
return [];
}
/**
* Return list of apps for Heroku integration
* @param {Object} obj
@@ -154,6 +151,7 @@ const getAppsVercel = async ({
await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
...(integrationAuth?.teamId
? {
@@ -191,7 +189,8 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
'Accept-Encoding': 'application/json'
}
})
).data;

View File

@@ -141,7 +141,7 @@ const exchangeCodeAzure = async ({
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
scope: 'https://vault.azure.net/.default openid offline_access', // TODO: do we need all these permissions?
scope: 'https://vault.azure.net/.default openid offline_access',
client_id: CLIENT_ID_AZURE,
client_secret: CLIENT_SECRET_AZURE,
redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback`

View File

@@ -1,10 +1,21 @@
import axios from "axios";
import * as Sentry from "@sentry/node";
import _ from 'lodash';
import AWS from 'aws-sdk';
import {
SecretsManagerClient,
UpdateSecretCommand,
CreateSecretCommand,
GetSecretValueCommand,
ResourceNotFoundException
} from '@aws-sdk/client-secrets-manager';
import { Octokit } from "@octokit/rest";
import sodium from "libsodium-wrappers";
import { IIntegration, IIntegrationAuth } from "../models";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -27,17 +38,20 @@ import { access, appendFile } from "fs";
* @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.accessId - access id for integration
* @param {String} obj.accessToken - access token for integration
*/
const syncSecrets = async ({
integration,
integrationAuth,
secrets,
accessId,
accessToken,
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: any;
accessId: string | null;
accessToken: string;
}) => {
try {
@@ -49,6 +63,22 @@ const syncSecrets = async ({
accessToken
});
break;
case INTEGRATION_AWS_PARAMETER_STORE:
await syncSecretsAWSParameterStore({
integration,
secrets,
accessId,
accessToken
});
break;
case INTEGRATION_AWS_SECRET_MANAGER:
await syncSecretsAWSSecretManager({
integration,
secrets,
accessId,
accessToken
});
break;
case INTEGRATION_HEROKU:
await syncSecretsHeroku({
integration,
@@ -252,6 +282,188 @@ const syncSecretsAzureKeyVault = async ({
}
};
/**
* Sync/push [secrets] to AWS parameter store
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessId - access id for AWS parameter store integration
* @param {String} obj.accessToken - access token for AWS parameter store integration
*/
const syncSecretsAWSParameterStore = async ({
integration,
secrets,
accessId,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessId: string | null;
accessToken: string;
}) => {
try {
if (!accessId) return;
AWS.config.update({
region: integration.region,
accessKeyId: accessId,
secretAccessKey: accessToken
});
const ssm = new AWS.SSM({
apiVersion: '2014-11-06',
region: integration.region
});
const params = {
Path: integration.path,
Recursive: true,
WithDecryption: true
};
const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters
let awsParameterStoreSecretsObj: {
[key: string]: any // TODO: fix type
} = {};
if (parameterList) {
awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => ({
...obj,
[secret.Name.split("/").pop()]: secret
}), {});
}
// Identify secrets to create
Object.keys(secrets).map(async (key) => {
if (!(key in awsParameterStoreSecretsObj)) {
// case: secret does not exist in AWS parameter store
// -> create secret
await ssm.putParameter({
Name: `${integration.path}${key}`,
Type: 'SecureString',
Value: secrets[key],
Overwrite: true
}).promise();
} else {
// case: secret exists in AWS parameter store
if (awsParameterStoreSecretsObj[key].Value !== secrets[key]) {
// case: secret value doesn't match one in AWS parameter store
// -> update secret
await ssm.putParameter({
Name: `${integration.path}${key}`,
Type: 'SecureString',
Value: secrets[key],
Overwrite: true
}).promise();
}
}
});
// Identify secrets to delete
Object.keys(awsParameterStoreSecretsObj).map(async (key) => {
if (!(key in secrets)) {
// case:
// -> delete secret
await ssm.deleteParameter({
Name: awsParameterStoreSecretsObj[key].Name
}).promise();
}
});
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to AWS Parameter Store');
}
}
/**
* Sync/push [secrets] to AWS secret manager
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessId - access id for AWS secret manager integration
* @param {String} obj.accessToken - access token for AWS secret manager integration
*/
const syncSecretsAWSSecretManager = async ({
integration,
secrets,
accessId,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessId: string | null;
accessToken: string;
}) => {
let secretsManager;
try {
if (!accessId) return;
AWS.config.update({
region: integration.region,
accessKeyId: accessId,
secretAccessKey: accessToken
});
secretsManager = new SecretsManagerClient({
region: integration.region,
credentials: {
accessKeyId: accessId,
secretAccessKey: accessToken
}
});
const awsSecretManagerSecret = await secretsManager.send(
new GetSecretValueCommand({
SecretId: integration.app
})
);
let awsSecretManagerSecretObj: { [key: string]: any } = {};
if (awsSecretManagerSecret?.SecretString) {
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString);
}
if (!_.isEqual(awsSecretManagerSecretObj, secrets)) {
await secretsManager.send(new UpdateSecretCommand({
SecretId: integration.app,
SecretString: JSON.stringify(secrets)
}));
}
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined
});
} catch (err) {
if (err instanceof ResourceNotFoundException && secretsManager) {
await secretsManager.send(new CreateSecretCommand({
Name: integration.app,
SecretString: JSON.stringify(secrets)
}));
} else {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to AWS Secret Manager');
}
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined
});
}
}
/**
* Sync/push [secrets] to Heroku app named [integration.app]
* @param {Object} obj
@@ -349,40 +561,30 @@ const syncSecretsVercel = async ({
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
)
).data.envs
.filter((secret: VercelSecret) =>
secret.target.includes(integration.targetEnvironment)
)
.map(
async (secret: VercelSecret) =>
(
await axios.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
)
).data
)
)
).reduce(
(obj: any, secret: any) => ({
...obj,
[secret.key]: secret,
}),
{}
);
Authorization: `Bearer ${accessToken}`
}
}
))
.data
.envs
.filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment))
.map(async (secret: VercelSecret) => (await axios.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
)).data)
)).reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret
}), {});
const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = [];
const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = [];
// Identify secrets to create
Object.keys(secrets).map((key) => {

View File

@@ -45,9 +45,10 @@ const requireIntegrationAuthorizationAuth = ({
req.integrationAuth = integrationAuth;
if (attachAccessToken) {
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
const access = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString()
});
req.accessToken = access.accessToken;
}
return next();

View File

@@ -1,6 +1,8 @@
import { Schema, model, Types } from "mongoose";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -19,15 +21,19 @@ export interface IIntegration {
owner: string;
targetEnvironment: string;
appId: string;
path: string;
region: string;
integration:
| "heroku"
| "vercel"
| "netlify"
| "github"
| "render"
| "flyio"
| "azure-key-vault"
| "circleci";
| 'azure-key-vault'
| 'aws-parameter-store'
| 'aws-secret-manager'
| 'heroku'
| 'vercel'
| 'netlify'
| 'github'
| 'render'
| 'flyio'
| 'circleci';
integrationAuth: Types.ObjectId;
}
@@ -68,10 +74,32 @@ const integrationSchema = new Schema<IIntegration>(
type: String,
default: null,
},
path: {
// aws-parameter-store-specific path
type: String,
default: null
},
region: {
// aws-parameter-store-specific path
type: String,
default: null
},
path: {
// aws-parameter-store-specific path
type: String,
default: null
},
region: {
// aws-parameter-store-specific path
type: String,
default: null
},
integration: {
type: String,
enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,

View File

@@ -1,30 +1,29 @@
import { Schema, model, Types } from "mongoose";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
} from "../variables";
export interface IIntegrationAuth {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration:
| "heroku"
| "vercel"
| "netlify"
| "github"
| "render"
| "flyio"
| "azure-key-vault"
| "circleci";
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | '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
accessCiphertext?: string;
accessIV?: string;
accessTag?: string;
@@ -42,10 +41,14 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
type: String,
enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
],
required: true,
@@ -70,6 +73,30 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
type: String,
select: false,
},
accessIdCiphertext: {
type: String,
select: false
},
accessIdIV: {
type: String,
select: false
},
accessIdTag: {
type: String,
select: false
},
accessIdCiphertext: {
type: String,
select: false
},
accessIdIV: {
type: String,
select: false
},
accessIdTag: {
type: String,
select: false
},
accessCiphertext: {
type: String,
select: false,

View File

@@ -109,6 +109,9 @@ const secretSchema = new Schema<ISecret>(
}
);
secretSchema.index({ tags: 1 }, { background: true })
const Secret = model<ISecret>('Secret', secretSchema);
export default Secret;

View File

@@ -20,12 +20,14 @@ router.post( // new: add new integration for integration auth
location: 'body'
}),
body('integrationAuthId').exists().isString().trim(),
body('app').isString().trim(),
body('app').trim(),
body('isActive').exists().isBoolean(),
body('appId').trim(),
body('sourceEnvironment').trim(),
body('targetEnvironment').trim(),
body('owner').trim(),
body('path').trim(),
body('region').trim(),
validateRequest,
integrationController.createIntegration
);

View File

@@ -57,6 +57,7 @@ router.post(
location: 'body'
}),
body('workspaceId').exists().trim().notEmpty(),
body('accessId').trim(),
body('accessToken').exists().trim().notEmpty(),
body('integration').exists().trim().notEmpty(),
validateRequest,

View File

@@ -112,26 +112,30 @@ class IntegrationService {
}
/**
* Encrypt access token [accessToken] using the bot's copy
* of the workspace key for workspace belonging to integration auth
* Encrypt access token [accessToken] and (optionally) access id using the
* bot's copy of the workspace key for workspace belonging to integration auth
* with id [integrationAuthId]
* @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.accessId - access id
* @param {String} obj.accessToken - access token
* @param {Date} obj.accessExpiresAt - expiration date of access token
* @returns {IntegrationAuth} - updated integration auth
*/
static async setIntegrationAuthAccess({
integrationAuthId,
accessId,
accessToken,
accessExpiresAt
}: {
integrationAuthId: string;
accessId: string | null;
accessToken: string;
accessExpiresAt: Date | undefined;
}) {
return await setIntegrationAuthAccessHelper({
integrationAuthId,
accessId,
accessToken,
accessExpiresAt
});

View File

@@ -6,10 +6,9 @@
<title>Email Verification</title>
</head>
<body>
<h2>Infisical</h2>
<h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
<h2>{{code}}</h2>
<h1>{{code}}</h1>
<p>Questions about setting up Infisical? Email us at support@infisical.com</p>
</body>
</html>

View File

@@ -7,12 +7,10 @@
<title>Organization Invitation</title>
</head>
<body>
<h2>Infisical</h2>
<h2>Join your team on Infisical</h2>
<p>{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}</p>
<h2>Join your organization on Infisical</h2>
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Join now</a>
<h3>What is Infisical?</h3>
<p>Infisical is a simple end-to-end encrypted solution that enables teams to sync and manage their environment
variables.</p>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
</body>
</html>

View File

@@ -6,7 +6,6 @@
<title>Account Recovery</title>
</head>
<body>
<h2>Infisical</h2>
<h2>Reset your password</h2>
<p>Someone requested a password reset.</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>

View File

@@ -6,11 +6,10 @@
<title>Project Invitation</title>
</head>
<body>
<h2>Infisical</h2>
<h2>Join your team on Infisical</h2>
<p>{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical workspace — {{workspaceName}}</p>
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}</p>
<a href="{{callback_url}}">Join now</a>
<h3>What is Infisical?</h3>
<p>Infisical is a simple end-to-end encrypted solution that enables teams to sync and manage their environment variables.</p>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
</body>
</html>

View File

@@ -7,6 +7,8 @@ import {
} from "./environment";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@@ -57,6 +59,8 @@ export {
ENV_PROD,
ENV_SET,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,

View File

@@ -11,6 +11,8 @@ import {
// integrations
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
const INTEGRATION_HEROKU = "heroku";
const INTEGRATION_VERCEL = "vercel";
const INTEGRATION_NETLIFY = "netlify";
@@ -50,6 +52,79 @@ const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
const INTEGRATION_OPTIONS = [
{
name: 'Heroku',
slug: 'heroku',
image: 'Heroku.png',
isAvailable: true,
type: 'oauth',
clientId: CLIENT_ID_HEROKU,
docsLink: ''
},
{
name: 'Vercel',
slug: 'vercel',
image: 'Vercel.png',
isAvailable: true,
type: 'oauth',
clientId: '',
clientSlug: CLIENT_SLUG_VERCEL,
docsLink: ''
},
{
name: 'Netlify',
slug: 'netlify',
image: 'Netlify.png',
isAvailable: true,
type: 'oauth',
clientId: CLIENT_ID_NETLIFY,
docsLink: ''
},
{
name: 'GitHub',
slug: 'github',
image: 'GitHub.png',
isAvailable: true,
type: 'oauth',
clientId: CLIENT_ID_GITHUB,
docsLink: ''
},
{
name: 'Render',
slug: 'render',
image: 'Render.png',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'Fly.io',
slug: 'flyio',
image: 'Flyio.svg',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'AWS Parameter Store',
slug: 'aws-parameter-store',
image: 'Amazon Web Services.png',
isAvailable: true,
type: 'custom',
clientId: '',
docsLink: ''
},
{
name: 'AWS Secret Manager',
slug: 'aws-secret-manager',
image: 'Amazon Web Services.png',
isAvailable: true,
type: 'custom',
clientId: '',
docsLink: ''
},
{
name: 'Azure Key Vault',
slug: 'azure-key-vault',
@@ -60,110 +135,39 @@ const INTEGRATION_OPTIONS = [
tenantId: TENANT_ID_AZURE,
docsLink: ''
},
{
name: "Heroku",
slug: "heroku",
image: "Heroku.png",
isAvailable: true,
type: "oauth",
clientId: CLIENT_ID_HEROKU,
docsLink: "",
},
{
name: "Vercel",
slug: "vercel",
image: "Vercel.png",
isAvailable: true,
type: "oauth",
clientId: "",
clientSlug: CLIENT_SLUG_VERCEL,
docsLink: "",
},
{
name: "Netlify",
slug: "netlify",
image: "Netlify.png",
isAvailable: true,
type: "oauth",
clientId: CLIENT_ID_NETLIFY,
docsLink: "",
},
{
name: "GitHub",
slug: "github",
image: "GitHub.png",
isAvailable: true,
type: "oauth",
clientId: CLIENT_ID_GITHUB,
docsLink: "",
},
{
name: "Render",
slug: "render",
image: "Render.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
},
{
name: "Fly.io",
slug: "flyio",
image: "Flyio.svg",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
},
{
name: "Google Cloud Platform",
slug: "gcp",
image: "Google Cloud Platform.png",
isAvailable: false,
type: "",
clientId: "",
docsLink: "",
},
{
name: "Amazon Web Services",
slug: "aws",
image: "Amazon Web Services.png",
isAvailable: false,
type: "",
clientId: "",
docsLink: "",
},
{
name: "Microsoft Azure",
slug: "azure",
image: "Microsoft Azure.png",
isAvailable: false,
type: "",
clientId: "",
docsLink: "",
},
{
name: "Travis CI",
slug: "travisci",
image: "Travis CI.png",
isAvailable: false,
type: "",
clientId: "",
docsLink: "",
},
{
name: "Circle CI",
slug: "circleci",
image: "Circle CI.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
},
];
{
name: 'Google Cloud Platform',
slug: 'gcp',
image: 'Google Cloud Platform.png',
isAvailable: false,
type: '',
clientId: '',
docsLink: ''
},
{
name: 'Travis CI',
slug: 'travisci',
image: 'Travis CI.png',
isAvailable: false,
type: '',
clientId: '',
docsLink: ''
},
{
name: 'Circle CI',
slug: 'circleci',
image: 'Circle CI.png',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
}
]
export {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,

View File

@@ -166,3 +166,24 @@ func CallIsAuthenticated(httpClient *resty.Client) bool {
return true
}
func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessibleEnvironmentsRequest) (GetAccessibleEnvironmentsResponse, error) {
var accessibleEnvironmentsResponse GetAccessibleEnvironmentsResponse
response, err := httpClient.
R().
SetResult(&accessibleEnvironmentsResponse).
SetHeader("User-Agent", USER_AGENT).
Get(fmt.Sprintf("%v/v2/workspace/%s/environments", config.INFISICAL_URL, request.WorkspaceId))
log.Debugln(fmt.Errorf("CallGetAccessibleEnvironments: Unsuccessful response: [response=%v]", response))
if err != nil {
return GetAccessibleEnvironmentsResponse{}, err
}
if response.IsError() {
return GetAccessibleEnvironmentsResponse{}, fmt.Errorf("CallGetAccessibleEnvironments: Unsuccessful response: [response=%v]", response)
}
return accessibleEnvironmentsResponse, nil
}

View File

@@ -250,3 +250,15 @@ type GetServiceTokenDetailsResponse struct {
UpdatedAt time.Time `json:"updatedAt"`
V int `json:"__v"`
}
type GetAccessibleEnvironmentsRequest struct {
WorkspaceId string `json:"workspaceId"`
}
type GetAccessibleEnvironmentsResponse struct {
AccessibleEnvironments []struct {
Name string `json:"name"`
Slug string `json:"slug"`
IsWriteDenied bool `json:"isWriteDenied"`
} `json:"accessibleEnvironments"`
}

45
cli/packages/cmd/reset.go Normal file
View File

@@ -0,0 +1,45 @@
/*
Copyright (c) 2023 Infisical Inc.
*/
package cmd
import (
"os"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/spf13/cobra"
)
var resetCmd = &cobra.Command{
Use: "reset",
Short: "Used delete all Infisical related data on your machine",
DisableFlagsInUseLine: true,
Example: "infisical reset",
Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
toggleDebug(cmd, args)
},
Run: func(cmd *cobra.Command, args []string) {
// delete config
_, pathToDir, err := util.GetFullConfigFilePath()
if err != nil {
util.HandleError(err)
}
os.RemoveAll(pathToDir)
// delete keyring
keyringInstance, err := util.GetKeyRing()
if err != nil {
util.HandleError(err)
}
keyringInstance.Remove(util.KEYRING_SERVICE_NAME)
util.PrintSuccessMessage("Reset successful")
},
}
func init() {
rootCmd.AddCommand(resetCmd)
}

View File

@@ -64,10 +64,6 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
// if !util.IsSecretEnvironmentValid(envName) {
// util.PrintMessageAndExit("Invalid environment name passed. Environment names can only be prod, dev, test or staging")
// }
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
if err != nil {
util.HandleError(err, "Unable to parse flag")

View File

@@ -512,7 +512,7 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
if len(listOfTagNames) == 0 {
fmt.Printf("\n%s \n", strings.Join(listOfKeyValue, "\n \n"))
} else {
fmt.Printf("\n\n\n%s\n \n%s \n", heading, strings.Join(listOfKeyValue, "\n \n"))
fmt.Printf("\n\n\n%s \n%s \n", heading, strings.Join(listOfKeyValue, "\n \n"))
}
}
}

View File

@@ -27,6 +27,10 @@ func PrintWarning(message string) {
color.New(color.FgYellow).Fprintf(os.Stderr, "Warning: %v \n", message)
}
func PrintSuccessMessage(message string) {
color.New(color.FgGreen).Println(message)
}
func PrintMessageAndExit(messages ...string) {
if len(messages) > 0 {
for _, message := range messages {

View File

@@ -130,6 +130,12 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
return nil, err
}
// Verify environment
err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials)
if err != nil {
return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
}
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment)
log.Debugf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
@@ -156,6 +162,33 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
return secretsToReturn, errorToReturn
}
func ValidateEnvironmentName(environmentName string, workspaceId string, userLoggedInDetails models.UserCredentials) error {
httpClient := resty.New()
httpClient.SetAuthToken(userLoggedInDetails.JTWToken).
SetHeader("Accept", "application/json")
response, err := api.CallGetAccessibleEnvironments(httpClient, api.GetAccessibleEnvironmentsRequest{WorkspaceId: workspaceId})
if err != nil {
return err
}
listOfEnvSlugs := []string{}
mapOfEnvSlugs := make(map[string]interface{})
for _, environment := range response.AccessibleEnvironments {
listOfEnvSlugs = append(listOfEnvSlugs, environment.Slug)
mapOfEnvSlugs[environment.Slug] = environment
}
_, exists := mapOfEnvSlugs[environmentName]
if !exists {
HandleError(fmt.Errorf("the environment [%s] does not exist in project with [id=%s]. Only [%s] are available", environmentName, workspaceId, strings.Join(listOfEnvSlugs, ",")))
}
return nil
}
func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string {
if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found {
return value

View File

@@ -0,0 +1,11 @@
---
title: "infisical reset"
description: "Reset Infisical"
---
```bash
infisical reset
```
## Description
This command provides a way to clear all Infisical-generated configuration data, effectively resetting the software to its default settings. This can be an effective way to address any persistent issues that arise while using the CLI.

View File

@@ -87,4 +87,25 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
Default value: `dev`
</Accordion>
</Accordion>
</Accordion>
<Accordion title="infisical secrets generate-example-env">
This command allows you to generate an example .env file from your secrets and with their associated comments and tags. This is useful when you would like to let
others who work on the project but do not use Infisical become aware of the required environment variables and their intended values.
To place default values in your example .env file, you can simply include the syntax `DEFAULT:<value>` within your secret's comment in Infisical. This will result in the specified value being extracted and utilized as the default.
```bash
$ infisical secrets generate-example-env
## Example
$ infisical secrets generate-example-env > .example-env
```
### Flags
<Accordion title="--env">
Used to select the environment name on which actions should be taken on
Default value: `dev`
</Accordion>
</Accordion>

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 KiB

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 KiB

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 403 KiB

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

After

Width:  |  Height:  |  Size: 421 KiB

View File

@@ -0,0 +1,75 @@
---
title: "AWS Parameter Store"
description: "How to automatically sync secrets from Infisical to your AWS Parameter Store."
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Set up AWS and have/create an IAM user
## Grant the IAM user permissions to access AWS Parameter Store
Navigate to your IAM user permissions and add a permission policy to grant access to AWS Parameter Store.
![integration IAM 1](../../images/integrations-aws-iam-1.png)
![integration IAM 2](../../images/integrations-aws-parameter-store-iam-2.png)
![integrations IAM 3](../../images/integrations-aws-parameter-store-iam-3.png)
For better security, here's a custom policy containing the minimum permissions required by Infisical to sync secrets to AWS Parameter Store for the IAM user that you can use:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSSMAccess",
"Effect": "Allow",
"Action": [
"ssm:PutParameter",
"ssm:DeleteParameter",
"ssm:GetParametersByPath",
"ssm:DeleteParameters"
],
"Resource": "*"
}
]
}
```
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Authorize Infisical for AWS Parameter store
Obtain a AWS access key ID and secret access key for your IAM user in IAM > Users > User > Security credentials > Access keys
![access key 1](../../images/integrations-aws-access-key-1.png)
![access key 2](../../images/integrations-aws-access-key-2.png)
![access key 3](../../images/integrations-aws-access-key-3.png)
Press on the AWS Parameter Store tile and input your AWS access key ID and secret access key from the previous step.
![integration auth](../../images/integrations-aws-parameter-store-auth.png)
<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 AWS Parameter Store region and indicate the path for your secrets. Then, press create integration to start syncing secrets to AWS Parameter Store.
![integration create](../../images/integrations-aws-parameter-store-create.png)
<Tip>
Infisical requires you to add a path for your secrets to be stored in AWS
Parameter Store and recommends setting the path structure to
`/[project_name]/[environment]/` according to best practices. This enables a
secret like `TEST` to be stored as `/[project_name]/[environment]/TEST` in AWS
Parameter Store.
</Tip>

View File

@@ -0,0 +1,73 @@
---
title: "AWS Secret Manager"
description: "How to automatically sync secrets from Infisical to your AWS Secret Manager."
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Set up AWS and have/create an IAM user
## Grant the IAM user permissions to access AWS Secret Manager
Navigate to your IAM user permissions and add a permission policy to grant access to AWS Secret Manager.
![integration IAM 1](../../images/integrations-aws-iam-1.png)
![integration IAM 2](../../images/integrations-aws-secret-manager-iam-2.png)
![integrations IAM 3](../../images/integrations-aws-secret-manager-iam-3.png)
For better security, here's a custom policy containing the minimum permissions required by Infisical to sync secrets to AWS Secret Manager for the IAM user that you can use:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSecretsManagerAccess",
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:CreateSecret",
"secretsmanager:UpdateSecret"
],
"Resource": "*"
}
]
}
```
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Authorize Infisical for AWS Secret Manager
Obtain a AWS access key ID and secret access key for your IAM user in IAM > Users > User > Security credentials > Access keys
![access key 1](../../images/integrations-aws-access-key-1.png)
![access key 2](../../images/integrations-aws-access-key-2.png)
![access key 3](../../images/integrations-aws-access-key-3.png)
Press on the AWS Secret Manager tile and input your AWS access key ID and secret access key from the previous step.
![integration auth](../../images/integrations-aws-secret-manager-auth.png)
<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 AWS Secret Manager region and under which secret name. Then, press create integration to start syncing secrets to AWS Secret Manager.
![integration create](../../images/integrations-aws-secret-manager-create.png)
<Info>
Infisical currently syncs environment variables to AWS Secret Manager as
key-value pairs under one secret. We're actively exploring ways to help users
group environment variable key-pairs under multiple secrets for greater
control.
</Info>

View File

@@ -31,6 +31,7 @@ Press on the Fly.io tile and input your Fly.io access token to grant Infisical a
## Start integration
Select which Infisical environment secrets you want to sync to which Fly.io app and press start integration to start syncing secrets to Fly.io.
Select which Infisical environment secrets you want to sync to which Fly.io app and press create integration to start syncing secrets to Fly.io.
![integrations fly](../../images/integrations-flyio-create.png)
![integrations fly](../../images/integrations-flyio.png)

View File

@@ -18,13 +18,15 @@ Press on the Heroku tile and grant Infisical access to your Heroku account.
![integrations heroku authorization](../../images/integrations-heroku-auth.png)
<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.
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 Heroku app and press start integration to start syncing secrets to Heroku.
Select which Infisical environment secrets you want to sync to which Heroku app and press create integration to start syncing secrets to Heroku.
![integrations heroku](../../images/integrations-heroku-create.png)
![integrations heroku](../../images/integrations-heroku.png)

View File

@@ -4,7 +4,9 @@ description: "How to automatically sync secrets from Infisical into your Netlify
---
<Warning>
Infisical integrates with Netlify's new environment variable experience. If your site uses Netlify's old environment variable experience, you'll have to upgrade it to the new one to use this integration.
Infisical integrates with Netlify's new environment variable experience. If
your site uses Netlify's old environment variable experience, you'll have to
upgrade it to the new one to use this integration.
</Warning>
Prerequisites:
@@ -22,12 +24,15 @@ Press on the Netlify tile and grant Infisical access to your Netlify account.
![integrations netlify authorization](../../images/integrations-netlify-auth.png)
<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.
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 Netlify app and context. Lastly, press start integration to start syncing secrets to Netlify.
Select which Infisical environment secrets you want to sync to which Netlify app and context. Lastly, press create integration to start syncing secrets to Netlify.
![integrations netlify](../../images/integrations-netlify.png)
![integrations netlify](../../images/integrations-netlify-create.png)
![integrations netlify](../../images/integrations-netlify.png)

View File

@@ -31,6 +31,7 @@ Press on the Render tile and input your Render API Key to grant Infisical access
## Start integration
Select which Infisical environment secrets you want to sync to which Render service and press start integration to start syncing secrets to Render.
Select which Infisical environment secrets you want to sync to which Render service and press create integration to start syncing secrets to Render.
![integrations heroku](../../images/integrations-render.png)
![integrations render](../../images/integrations-render-create.png)
![integrations render](../../images/integrations-render.png)

View File

@@ -17,8 +17,16 @@ Press on the Vercel tile and grant Infisical access to your Vercel account.
![integrations vercel authorization](../../images/integrations-vercel-auth.png)
<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 Vercel app and environment. Lastly, press start integration to start syncing secrets to Vercel.
Select which Infisical environment secrets you want to sync to which Vercel app and environment. Lastly, press create integration to start syncing secrets to Vercel.
![integrations vercel](../../images/integrations-vercel-create.png)
![integrations vercel](../../images/integrations-vercel.png)

View File

@@ -7,38 +7,40 @@ Integrations allow environment variables to be synced from Infisical into your l
Missing an integration? Throw in a [request](https://github.com/Infisical/infisical/issues).
| Integration | Type | Status |
| -------------------------------------------------------- | --------- | ----------- |
| [Docker](/integrations/platforms/docker) | Platform | Available |
| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available |
| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available |
| [PM2](/integrations/platforms/pm2) | Platform | Available |
| [Heroku](/integrations/cloud/heroku) | Cloud | Available |
| [Vercel](/integrations/cloud/vercel) | Cloud | Available |
| [Netlify](/integrations/cloud/netlify) | Cloud | Available |
| [Render](/integrations/cloud/render) | Cloud | Available |
| [Fly.io](/integrations/cloud/flyio) | Cloud | Available |
| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available |
| [React](/integrations/frameworks/react) | Framework | Available |
| [Vue](/integrations/frameworks/vue) | Framework | Available |
| [Express](/integrations/frameworks/express) | Framework | Available |
| [Next.js](/integrations/frameworks/nextjs) | Framework | Available |
| [NestJS](/integrations/frameworks/nestjs) | Framework | Available |
| [Nuxt](/integrations/frameworks/nuxt) | Framework | Available |
| [Gatsby](/integrations/frameworks/gatsby) | Framework | Available |
| [Remix](/integrations/frameworks/remix) | Framework | Available |
| [Vite](/integrations/frameworks/vite) | Framework | Available |
| [Fiber](/integrations/frameworks/fiber) | Framework | Available |
| [Django](/integrations/frameworks/django) | Framework | Available |
| [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 |
| [GitLab Pipeline](/integrations/cicd/gitlab) | CI/CD | Available |
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Coming soon |
| TravisCI | CI/CD | Coming soon |
| GitHub Actions | CI/CD | Coming soon |
| Jenkins | CI/CD | Coming soon |
| Integration | Type | Status |
| -------------------------------------------------------------- | --------- | ----------- |
| [Docker](/integrations/platforms/docker) | Platform | Available |
| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available |
| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available |
| [PM2](/integrations/platforms/pm2) | Platform | Available |
| [Heroku](/integrations/cloud/heroku) | Cloud | Available |
| [Vercel](/integrations/cloud/vercel) | Cloud | Available |
| [Netlify](/integrations/cloud/netlify) | Cloud | Available |
| [Render](/integrations/cloud/render) | Cloud | Available |
| [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 |
| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available |
| [React](/integrations/frameworks/react) | Framework | Available |
| [Vue](/integrations/frameworks/vue) | Framework | Available |
| [Express](/integrations/frameworks/express) | Framework | Available |
| [Next.js](/integrations/frameworks/nextjs) | Framework | Available |
| [NestJS](/integrations/frameworks/nestjs) | Framework | Available |
| [Nuxt](/integrations/frameworks/nuxt) | Framework | Available |
| [Gatsby](/integrations/frameworks/gatsby) | Framework | Available |
| [Remix](/integrations/frameworks/remix) | Framework | Available |
| [Vite](/integrations/frameworks/vite) | Framework | Available |
| [Fiber](/integrations/frameworks/fiber) | Framework | Available |
| [Django](/integrations/frameworks/django) | Framework | Available |
| [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 |
| [GitLab Pipeline](/integrations/cicd/gitlab) | CI/CD | Available |
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Coming soon |
| TravisCI | CI/CD | Coming soon |
| GitHub Actions | CI/CD | Coming soon |
| Jenkins | CI/CD | Coming soon |

View File

@@ -114,7 +114,8 @@
"cli/commands/run",
"cli/commands/secrets",
"cli/commands/export",
"cli/commands/vault"
"cli/commands/vault",
"cli/commands/reset"
]
},
"cli/faq"
@@ -220,7 +221,9 @@
"integrations/cloud/vercel",
"integrations/cloud/netlify",
"integrations/cloud/render",
"integrations/cloud/flyio"
"integrations/cloud/flyio",
"integrations/cloud/aws-parameter-store",
"integrations/cloud/aws-secret-manager"
]
},
{

View File

@@ -4,6 +4,8 @@ interface Mapping {
const integrationSlugNameMapping: Mapping = {
'azure-key-vault': 'Azure Key Vault',
'aws-parameter-store': 'AWS Parameter Store',
'aws-secret-manager': 'AWS Secret Manager',
'heroku': 'Heroku',
'vercel': 'Vercel',
'netlify': 'Netlify',

View File

@@ -1,6 +1,6 @@
{
"support": {
"slack": "[NEW] Join Slack Forum",
"slack": "Join Slack Forum",
"docs": "Read Docs",
"issue": "Open a Github Issue",
"email": "Send us an Email"

View File

@@ -246,9 +246,9 @@ const Layout = ({ children }: LayoutProps) => {
return (
<>
<div className="flex h-screen w-full flex-col overflow-x-hidden">
<div className="h-screen w-full flex-col overflow-x-hidden hidden md:flex">
<NavBarDashboard />
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row dark">
<aside className="w-full border-r border-mineshaft-500 bg-bunker-600 md:w-60">
<nav className="items-between flex h-full flex-col justify-between">
{/* <div className="py-6"></div> */}
@@ -368,10 +368,10 @@ const Layout = ({ children }: LayoutProps) => {
error={error}
loading={loading}
/>
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800">{children}</main>
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">{children}</main>
</div>
</div>
<div className="flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
<div className="flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 z-[200] md:hidden">
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
{` ${t('common:no-mobile')} `}

View File

@@ -34,7 +34,7 @@ const BottonRightPopup = ({
}: PopupProps): JSX.Element => {
return (
<div
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-md absolute bottom-0 right-0 mr-6 mb-6"
className="z-[100] drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-md absolute bottom-0 right-0 mr-6 mb-6"
role="alert"
>
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">

View File

@@ -18,7 +18,7 @@ const CommentField = ({
<div className="relative mt-4 px-4 pt-6">
<p className="text-sm text-bunker-300 pl-0.5">{t('dashboard:sidebar.comments')}</p>
<textarea
className="placeholder:text-bunker-400 h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
className="placeholder:text-bunker-400 dark:[color-scheme:dark] h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
value={comment}
onChange={(e) => modifyComment(e.target.value, position)}
placeholder="Leave any comments here..."

View File

@@ -125,7 +125,7 @@ const DashboardInputField = ({
const error = startsWithNumber || isDuplicate;
return (
<div className={`relative flex-col w-full h-10 ${
<div title={value} className={`relative flex-col w-full h-10 ${
isSideBarOpen && 'bg-mineshaft-700 duration-200'
}`}>
<div
@@ -137,7 +137,7 @@ const DashboardInputField = ({
onChange={(e) => onChangeHandler(e.target.value, position)}
type={type}
value={value}
className='z-10 peer font-mono ph-no-capture bg-transparent py-2.5 caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'
className='z-10 peer ph-no-capture bg-transparent py-2.5 caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'
spellCheck="false"
placeholder='–'
/>
@@ -209,7 +209,7 @@ const DashboardInputField = ({
{value?.split('').map(() => (
<FontAwesomeIcon
key={guidGenerator()}
className="text-xxs mx-0.5"
className="text-xxs mr-0.5"
icon={faCircle}
/>
))}

View File

@@ -34,7 +34,10 @@ const colors = [
'bg-[#cb1c8d]/40',
'bg-[#badc58]/40',
'bg-[#ff5400]/40',
'bg-[#00bbf9]/40'
'bg-[#3AB0FF]/40',
'bg-[#6F1AB6]/40',
'bg-[#C40B13]/40',
'bg-[#332FD0]/40'
]
@@ -43,7 +46,10 @@ const colorsText = [
'text-[#f2c6e3]/70',
'text-[#eef6d5]/70',
'text-[#ffddcc]/70',
'text-[#f0fffd]/70'
'text-[#f0fffd]/70',
'text-[#FFE5F1]/70',
'text-[#FFDEDE]/70',
'text-[#DFF6FF]/70'
]
/**
@@ -95,9 +101,9 @@ const KeyPair = ({
}`}
>
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
<div className="w-2/12 border-r border-mineshaft-600 flex flex-row items-center">
<div className="w-1/5 border-r border-mineshaft-600 flex flex-row items-center">
<div className='text-bunker-400 text-xs flex items-center justify-center w-14 h-10 cursor-default'>{keyPair.pos + 1}</div>
<div className="flex items-center max-h-16">
<div className="flex items-center max-h-16 w-full">
<DashboardInputField
isCapitalized = {isCapitalized}
onChangeHandler={modifyKey}

View File

@@ -18,6 +18,8 @@ interface Integration {
isActive: boolean;
app: string | null;
appId: string | null;
path: string | null;
region: string | null;
createdAt: string;
updatedAt: string;
environment: string;
@@ -75,6 +77,8 @@ const IntegrationTile = ({
if (integration?.app) {
setIntegrationApp(integration.app);
} else if (integration?.path && integration?.region) {
setIntegrationApp(`${integration.path} (${integration.region})`);
} else if (tempApps.length > 0) {
setIntegrationApp(tempApps[0].name)
} else {

View File

@@ -14,6 +14,8 @@ interface Integration {
isActive: boolean;
app: string | null;
appId: string | null;
path: string | null;
region: string | null;
createdAt: string;
updatedAt: string;
environment: string;

View File

@@ -93,7 +93,7 @@ export default function Navbar() {
};
return (
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-50">
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-[61]">
<div className="m-auto flex justify-start items-center mx-4">
<div className="flex flex-row items-center">
<div className="flex justify-center py-4">
@@ -166,8 +166,8 @@ export default function Navbar() {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none">
<div className="px-1 py-1 ">
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-[65] ring-opacity-5 focus:outline-none">
<div className="px-1 py-1 z-[100]">
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
{t('nav:user.signed-in-as')}
</div>

View File

@@ -1,10 +1,7 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useWorkspace } from '@app/context';
import getOrganization from '@app/pages/api/organization/GetOrg';
import { useOrganization, useWorkspace } from '@app/context';
/**
* This is the component at the top of almost every page.
@@ -22,28 +19,15 @@ export default function NavHeader({
pageName: string;
isProjectRelated?: boolean;
}): JSX.Element {
const [orgName, setOrgName] = useState('');
const router = useRouter();
const projectId = String(router.query.id);
const { currentWorkspace } = useWorkspace();
useEffect(() => {
(async () => {
const orgId = localStorage.getItem('orgData.id');
const org = await getOrganization({
orgId: orgId || ''
});
setOrgName(org.name);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]);
const { currentOrg } = useOrganization();
return (
<div className="ml-6 flex flex-row items-center pt-8">
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
{orgName?.charAt(0)}
{currentOrg?.name?.charAt(0)}
</div>
<div className="text-sm font-semibold text-primary">{orgName}</div>
<div className="text-sm font-semibold text-primary">{currentOrg?.name}</div>
{isProjectRelated && (
<>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />

View File

@@ -4,7 +4,7 @@ const POSTHOG_HOST =
process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
const STRIPE_PRODUCT_PRO = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO!;
const STRIPE_PRODUCT_STARTER = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER!;
const {SITE_URL} = process.env
const SITE_URL = "https://app.infisical.com";
export {
ENV,
@@ -12,4 +12,5 @@ export {
POSTHOG_HOST,
SITE_URL,
STRIPE_PRODUCT_PRO,
STRIPE_PRODUCT_STARTER};
STRIPE_PRODUCT_STARTER
};

View File

@@ -24,7 +24,7 @@ const buttonVariants = cva(
{
variants: {
colorSchema: {
primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
primary: ['bg-primary', 'text-black', 'border-primary bg-opacity-80 hover:bg-opacity-100'],
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90']
},

View File

@@ -8,9 +8,9 @@ export type CardTitleProps = {
};
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
<div className={twMerge('px-6 py-4 font-sans text-xl font-medium', className)}>
<div className={twMerge('px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600', className)}>
{children}
{subTitle && <p className="py-1 text-sm font-normal text-gray-400">{subTitle}</p>}
{subTitle && <p className="pt-0.5 text-sm font-normal text-gray-400">{subTitle}</p>}
</div>
);

View File

@@ -28,8 +28,9 @@ export const Checkbox = ({
<div className="flex items-center font-inter text-bunker-300">
<CheckboxPrimitive.Root
className={twMerge(
'flex items-center justify-center w-5 h-5 mr-3 transition-all rounded shadow hover:bg-bunker-200 bg-bunker-300',
'flex items-center justify-center w-4 h-4 mr-3 transition-all rounded shadow border border-mineshaft-400 hover:bg-mineshaft-500 bg-mineshaft-600',
isDisabled && 'bg-bunker-400 hover:bg-bunker-400',
isChecked && 'bg-primary hover:bg-primary',
className
)}
required={isRequired}

View File

@@ -24,7 +24,7 @@ export const HoverObject = ({
</a>
</HoverCard.Trigger>
<HoverCard.Portal>
<HoverCard.Content className="HoverCardContent z-[50]" sideOffset={5}>
<HoverCard.Content className="HoverCardContent z-[300]" sideOffset={5}>
<div className='bg-bunker-700 border border-mineshaft-600 p-2 rounded-md drop-shadow-xl text-bunker-300'>
<div style={{ display: 'flex', flexDirection: 'column', gap: 15 }}>
<div>

View File

@@ -31,18 +31,20 @@ export const MenuItem = <T extends ElementType = 'button'>({
as: Item = 'button',
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef
inputRef,
...props
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => (
<li
className={twMerge(
'px-2 py-3 font-inter flex flex-col text-sm text-white transition-all rounded cursor-pointer hover:bg-gray-700',
isSelected && 'text-primary',
isDisabled && 'text-gray-500 hover:bg-transparent cursor-not-allowed',
'px-1 py-2.5 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-600 duration-50',
isSelected && 'bg-mineshaft-500',
isDisabled && 'hover:bg-transparent cursor-not-allowed',
className
)}
>
<Item type="button" role="menuitem" class="flex items-center" ref={inputRef}>
{icon && <span className="mr-2">{icon}</span>}
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}>
<div className={`${isSelected ? "visisble" : "invisible"} absolute w-[0.25rem] rounded-md h-8 bg-primary`}/>
{icon && <span className="mr-3 ml-4 w-5">{icon}</span>}
<span className="flex-grow text-left">{children}</span>
</Item>
{description && <span className="mt-2 text-xs">{description}</span>}

View File

@@ -25,7 +25,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
<Card
isRounded
className={twMerge(
'fixed top-1/2 left-1/2 max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn drop-shadow-2xl z-[90]',
'fixed top-1/2 left-1/2 max-w-lg border border-mineshaft-600 -translate-y-2/4 -translate-x-2/4 animate-popIn drop-shadow-2xl z-[90]',
className
)}
>
@@ -36,7 +36,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
<IconButton
variant="plain"
ariaLabel="close"
className="absolute top-3 right-3 rounded text-white hover:bg-gray-600"
className="absolute top-4 right-6 rounded text-bunker-400 hover:text-bunker-50"
>
<FontAwesomeIcon icon={faTimes} size="lg" className="cursor-pointer" />
</IconButton>

View File

@@ -12,13 +12,14 @@ type Props = {
className?: string;
dropdownContainerClassName?: string;
isLoading?: boolean;
position?: 'item-aligned' | 'popper';
};
export type SelectProps = SelectPrimitive.SelectProps & Props;
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
(
{ children, placeholder, className, isLoading, dropdownContainerClassName, ...props },
{ children, placeholder, className, isLoading, dropdownContainerClassName, position, ...props },
ref
): JSX.Element => {
return (
@@ -44,11 +45,13 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
dropdownContainerClassName
)}
position={position}
style={{ width: 'var(--radix-select-trigger-width)' }}
>
<SelectPrimitive.ScrollUpButton>
<FontAwesomeIcon icon={faChevronUp} size="sm" />
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport className="p-1.5">
<SelectPrimitive.Viewport className="p-1">
{isLoading ? (
<div className="flex items-center justify-center">
<Spinner size="xs" />
@@ -82,7 +85,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
{...props}
className={twMerge(
`relative flex cursor-pointer
select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
select-none items-center rounded-md py-2 pl-10 pr-4 mb-0.5 text-sm
outline-none transition-all hover:bg-mineshaft-500`,
isSelected && 'bg-primary',
isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-gray-600',
@@ -90,8 +93,8 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
)}
ref={forwardedRef}
>
<SelectPrimitive.ItemIndicator className="absolute left-3.5">
<FontAwesomeIcon icon={faCheck} size="sm" />
<SelectPrimitive.ItemIndicator className="absolute left-3.5 text-primary">
<FontAwesomeIcon icon={faCheck} />
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>

View File

@@ -0,0 +1,46 @@
import { createContext, ReactNode, useContext, useMemo } from 'react';
import { useGetOrganization } from '@app/hooks/api';
import { Organization } from '@app/hooks/api/types';
import { useWorkspace } from '../WorkspaceContext';
type TOrgContext = {
orgs?: Organization[];
currentOrg?: Organization;
isLoading: boolean;
};
const OrgContext = createContext<TOrgContext | null>(null);
type Props = {
children: ReactNode;
};
export const OrgProvider = ({ children }: Props): JSX.Element => {
const { currentWorkspace } = useWorkspace();
const { data: userOrgs, isLoading } = useGetOrganization();
const currentWsOrgID = currentWorkspace?.organization;
// memorize the workspace details for the context
const value = useMemo<TOrgContext>(
() => ({
orgs: userOrgs,
currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID),
isLoading
}),
[currentWsOrgID, userOrgs, isLoading]
);
return <OrgContext.Provider value={value}>{children}</OrgContext.Provider>;
};
export const useOrganization = () => {
const ctx = useContext(OrgContext);
if (!ctx) {
throw new Error('useOrganization to be used within <OrgContext.Provider>');
}
return ctx;
};

View File

@@ -0,0 +1 @@
export { OrgProvider, useOrganization } from './OrganizationContext';

View File

@@ -0,0 +1,38 @@
import { createContext, ReactNode, useContext, useMemo } from 'react';
import { useGetUser } from '@app/hooks/api';
import { User } from '@app/hooks/api/types';
type TUserContext = {
user: User;
isLoading: boolean;
};
const UserContext = createContext<TUserContext | null>(null);
type Props = {
children: ReactNode;
};
export const UserProvider = ({ children }: Props): JSX.Element => {
const { data, isLoading } = useGetUser();
// memorize the workspace details for the context
const value = useMemo<TUserContext>(() => {
return {
user: data!,
isLoading
};
}, [data, isLoading]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
export const useUser = () => {
const ctx = useContext(UserContext);
if (!ctx) {
throw new Error('useUser has to be used within <UserContext.Provider>');
}
return ctx;
};

View File

@@ -0,0 +1 @@
export { UserProvider, useUser } from './UserContext';

View File

@@ -23,9 +23,10 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
// memorize the workspace details for the context
const value = useMemo<TWorkspaceContext>(() => {
const wsId = workspaceId || localStorage.getItem('projectData.id');
return {
workspaces: ws || [],
currentWorkspace: (ws || []).find(({ _id: id }) => id === workspaceId),
currentWorkspace: (ws || []).find(({ _id: id }) => id === wsId),
isLoading
};
}, [ws, workspaceId, isLoading]);

View File

@@ -1,2 +1,5 @@
export { AuthProvider } from './AuthContext';
export { OrgProvider, useOrganization } from './OrganizationContext';
export { SubscriptionProvider, useSubscription } from './SubscriptionContext';
export { UserProvider, useUser } from './UserContext';
export { useWorkspace, WorkspaceProvider } from './WorkspaceContext';

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faX } from '@fortawesome/free-solid-svg-icons';
import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import getActionData from '@app/ee/api/secrets/GetActionData';
@@ -154,7 +154,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
tabIndex={0}
onClick={() => toggleSidebar('')}
>
<FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
</div>
</div>
<div className="flex flex-col px-4">
@@ -163,7 +163,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
actionMetaData?.name === 'deleteSecrets') &&
actionData?.map((item, id) => (
<div key={`secret.${id + 1}`}>
<div className="text-xs text-bunker-200 mt-4 pl-1">
<div className="text-xs text-bunker-200 mt-4 pl-1 ph-no-capture">
{item.newSecretVersion.key}
</div>
<DashboardInputField
@@ -183,7 +183,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
{item.newSecretVersion.key}
</div>
<div className="text-bunker-100 font-mono rounded-md overflow-hidden">
<div className="bg-red/30 px-2">
<div className="bg-red/30 px-2 ph-no-capture">
-{' '}
{patienceDiff(
item.oldSecretVersion.value.split(''),
@@ -201,7 +201,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
)
)}
</div>
<div className="bg-green-500/30 px-2">
<div className="bg-green-500/30 px-2 ph-no-capture">
+{' '}
{patienceDiff(
item.oldSecretVersion.value.split(''),

View File

@@ -113,7 +113,7 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
})}
</div>
<div className="">
<p className="break-words">
<p className="break-words ph-no-capture">
<span className="py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5">
Value:
</span>

View File

@@ -1,6 +1,8 @@
export * from './auth';
export * from './keys';
export * from './organization';
export * from './serviceTokens';
export * from './subscriptions';
export * from './tags';
export * from './users';
export * from './workspace';

View File

@@ -1 +1 @@
export { useGetUserWsKey } from './queries';
export { useGetUserWsKey, useUploadWsKey } from './queries';

View File

@@ -1,8 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { UserWsKeyPair } from './types';
import { UploadWsKeyDTO, UserWsKeyPair } from './types';
const encKeyKeys = {
getUserWorkspaceKey: (workspaceID: string) => ['worksapce-key-pair', { workspaceID }] as const
@@ -22,3 +22,10 @@ export const useGetUserWsKey = (workspaceID: string) =>
queryFn: () => fetchUserWsKey(workspaceID),
enabled: Boolean(workspaceID)
});
// mutations
export const useUploadWsKey = () =>
useMutation<{}, {}, UploadWsKeyDTO>({
mutationFn: ({ encryptedKey, nonce, userId, workspaceId }) =>
apiRequest.post(`/api/v1/key/${workspaceId}`, { key: { userId, encryptedKey, nonce } })
});

View File

@@ -20,3 +20,10 @@ export type Sender = {
lastName: string;
publicKey: string;
};
export type UploadWsKeyDTO = {
userId: string;
encryptedKey: string;
nonce: string;
workspaceId: string;
};

View File

@@ -0,0 +1 @@
export { useGetOrganization } from './queries';

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