Merge pull request #508 from Infisical/secrets-v3

Secrets V3 — Blind Indices (Query for Secrets by Name)
This commit is contained in:
BlackMagiq
2023-04-22 11:53:48 +03:00
committed by GitHub
84 changed files with 3620 additions and 588 deletions

View File

@@ -16,6 +16,7 @@
"@sentry/tracing": "^7.46.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"argon2": "^0.30.3",
"await-to-js": "^3.0.0",
"aws-sdk": "^2.1338.0",
"axios": "^1.1.3",
@@ -3581,6 +3582,14 @@
"@octokit/openapi-types": "^16.0.0"
}
},
"node_modules/@phc/format": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
"integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==",
"engines": {
"node": ">=10"
}
},
"node_modules/@posthog/plugin-scaffold": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@posthog/plugin-scaffold/-/plugin-scaffold-1.4.2.tgz",
@@ -3644,58 +3653,6 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"node_modules/@sentry-internal/tracing": {
"version": "7.45.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.45.0.tgz",
"integrity": "sha512-0aIDY2OvUX7k2XHaimOlWkboXoQvJ9dEKvfpu0Wh0YxfUTGPa+wplUdg3WVdkk018sq1L11MKmj4MPZyYUvXhw==",
"dependencies": {
"@sentry/core": "7.45.0",
"@sentry/types": "7.45.0",
"@sentry/utils": "7.45.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/tracing/node_modules/@sentry/core": {
"version": "7.45.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.45.0.tgz",
"integrity": "sha512-xJfdTS4lRmHvZI/A5MazdnKhBJFkisKu6G9EGNLlZLre+6W4PH5sb7QX4+xoBdqG7v10Jvdia112vi762ojO2w==",
"dependencies": {
"@sentry/types": "7.45.0",
"@sentry/utils": "7.45.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/tracing/node_modules/@sentry/types": {
"version": "7.45.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.45.0.tgz",
"integrity": "sha512-iFt7msfUK8LCodFF3RKUyaxy9tJv/gpWhzxUFyNxtuVwlpmd+q6mtsFGn8Af3pbpm8A+MKyz1ebMwXj0PQqknw==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/tracing/node_modules/@sentry/utils": {
"version": "7.45.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.45.0.tgz",
"integrity": "sha512-aTY7qqtNUudd09SH5DVSKMm3iQ6ZeWufduc0I9bPZe6UMM09BDc4KmjmrzRkdQ+VaOmHo7+v+HZKQk5f+AbuTQ==",
"dependencies": {
"@sentry/types": "7.45.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/tracing/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/core": {
"version": "7.46.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.46.0.tgz",
@@ -3714,24 +3671,6 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/node": {
"version": "7.45.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.45.0.tgz",
"integrity": "sha512-x8mq+DrJWpSi716Rap/2w70DKWD8vjl87Y70OYFu+Dn6CxWDHClObSxLzuJcE5lww0Sq9RnU6UHQWzjXSb/pVQ==",
"dependencies": {
"@sentry/types": "7.46.0",
"@sentry/utils": "7.46.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/core/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/node": {
"version": "7.46.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.46.0.tgz",
@@ -4728,6 +4667,20 @@
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"node_modules/argon2": {
"version": "0.30.3",
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.30.3.tgz",
"integrity": "sha512-DoH/kv8c9127ueJSBxAVJXinW9+EuPA3EMUxoV2sAY1qDE5H9BjTyVF/aD2XyHqbqUWabgBkIfcP3ZZuGhbJdg==",
"hasInstallScript": true,
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.10",
"@phc/format": "^1.0.0",
"node-addon-api": "^5.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -4800,9 +4753,9 @@
}
},
"node_modules/aws-sdk": {
"version": "2.1359.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1359.0.tgz",
"integrity": "sha512-uGNIU4czx8P0YITV8uhuLFhmyYvLWsFYINlHJX77/fea4VuTwcCGktYy2OEnrErp3FK9NHQvwXBxZCbY0lcxBg==",
"version": "2.1358.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1358.0.tgz",
"integrity": "sha512-ZolqFlnm0mDNgub7FGrVi7r5A1rw+58zZziKhlis3IxOtIpHdx4BQU5pH4htAMuD0Ct557p/dC/wmnZH/1Rc9Q==",
"dependencies": {
"buffer": "4.9.2",
"events": "1.1.1",
@@ -15949,6 +15902,11 @@
"@octokit/openapi-types": "^16.0.0"
}
},
"@phc/format": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
"integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="
},
"@posthog/plugin-scaffold": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@posthog/plugin-scaffold/-/plugin-scaffold-1.4.2.tgz",
@@ -16012,48 +15970,6 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"@sentry-internal/tracing": {
"version": "7.45.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.45.0.tgz",
"integrity": "sha512-0aIDY2OvUX7k2XHaimOlWkboXoQvJ9dEKvfpu0Wh0YxfUTGPa+wplUdg3WVdkk018sq1L11MKmj4MPZyYUvXhw==",
"requires": {
"@sentry/core": "7.45.0",
"@sentry/types": "7.45.0",
"@sentry/utils": "7.45.0",
"tslib": "^1.9.3"
},
"dependencies": {
"@sentry/core": {
"version": "7.45.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.45.0.tgz",
"integrity": "sha512-xJfdTS4lRmHvZI/A5MazdnKhBJFkisKu6G9EGNLlZLre+6W4PH5sb7QX4+xoBdqG7v10Jvdia112vi762ojO2w==",
"requires": {
"@sentry/types": "7.45.0",
"@sentry/utils": "7.45.0",
"tslib": "^1.9.3"
}
},
"@sentry/types": {
"version": "7.45.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.45.0.tgz",
"integrity": "sha512-iFt7msfUK8LCodFF3RKUyaxy9tJv/gpWhzxUFyNxtuVwlpmd+q6mtsFGn8Af3pbpm8A+MKyz1ebMwXj0PQqknw=="
},
"@sentry/utils": {
"version": "7.45.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.45.0.tgz",
"integrity": "sha512-aTY7qqtNUudd09SH5DVSKMm3iQ6ZeWufduc0I9bPZe6UMM09BDc4KmjmrzRkdQ+VaOmHo7+v+HZKQk5f+AbuTQ==",
"requires": {
"@sentry/types": "7.45.0",
"tslib": "^1.9.3"
}
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@sentry/core": {
"version": "7.46.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.46.0.tgz",
@@ -16071,23 +15987,6 @@
}
}
},
"@sentry/node": {
"version": "7.45.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.45.0.tgz",
"integrity": "sha512-x8mq+DrJWpSi716Rap/2w70DKWD8vjl87Y70OYFu+Dn6CxWDHClObSxLzuJcE5lww0Sq9RnU6UHQWzjXSb/pVQ==",
"requires": {
"@sentry/types": "7.46.0",
"@sentry/utils": "7.46.0",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@sentry/node": {
"version": "7.46.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.46.0.tgz",
@@ -16168,27 +16067,6 @@
}
}
},
"@sentry/types": {
"version": "7.46.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.46.0.tgz",
"integrity": "sha512-2FMEMgt2h6u7AoELhNhu9L54GAh67KKfK2pJ1kEXJHmWxM9FSCkizjLs/t+49xtY7jEXr8qYq8bV967VfDPQ9g=="
},
"@sentry/utils": {
"version": "7.46.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.46.0.tgz",
"integrity": "sha512-elRezDAF84guMG0OVIIZEWm6wUpgbda4HGks98CFnPsrnMm3N1bdBI9XdlxYLtf+ir5KsGR5YlEIf/a0kRUwAQ==",
"requires": {
"@sentry/types": "7.46.0",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@sinclair/typebox": {
"version": "0.25.24",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
@@ -16884,6 +16762,16 @@
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"argon2": {
"version": "0.30.3",
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.30.3.tgz",
"integrity": "sha512-DoH/kv8c9127ueJSBxAVJXinW9+EuPA3EMUxoV2sAY1qDE5H9BjTyVF/aD2XyHqbqUWabgBkIfcP3ZZuGhbJdg==",
"requires": {
"@mapbox/node-pre-gyp": "^1.0.10",
"@phc/format": "^1.0.0",
"node-addon-api": "^5.0.0"
}
},
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -16938,9 +16826,9 @@
"integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="
},
"aws-sdk": {
"version": "2.1359.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1359.0.tgz",
"integrity": "sha512-uGNIU4czx8P0YITV8uhuLFhmyYvLWsFYINlHJX77/fea4VuTwcCGktYy2OEnrErp3FK9NHQvwXBxZCbY0lcxBg==",
"version": "2.1358.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1358.0.tgz",
"integrity": "sha512-ZolqFlnm0mDNgub7FGrVi7r5A1rw+58zZziKhlis3IxOtIpHdx4BQU5pH4htAMuD0Ct557p/dC/wmnZH/1Rc9Q==",
"requires": {
"buffer": "4.9.2",
"events": "1.1.1",

View File

@@ -8,6 +8,7 @@
"@sentry/node": "^7.41.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"argon2": "^0.30.3",
"await-to-js": "^3.0.0",
"aws-sdk": "^2.1338.0",
"axios": "^1.1.3",

View File

@@ -1532,7 +1532,15 @@
"/api/v1/invite-org/signup": {
"post": {
"description": "",
"parameters": [],
"parameters": [
{
"name": "host",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
@@ -2071,6 +2079,15 @@
"targetEnvironment": {
"example": "any"
},
"targetEnvironmentId": {
"example": "any"
},
"targetService": {
"example": "any"
},
"targetServiceId": {
"example": "any"
},
"owner": {
"example": "any"
},
@@ -2297,6 +2314,13 @@
"schema": {
"type": "string"
}
},
{
"name": "teamId",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
@@ -2309,6 +2333,107 @@
}
}
},
"/api/v1/integration-auth/{integrationAuthId}/teams": {
"get": {
"description": "",
"parameters": [
{
"name": "integrationAuthId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v1/integration-auth/{integrationAuthId}/vercel/branches": {
"get": {
"description": "",
"parameters": [
{
"name": "integrationAuthId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "appId",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v1/integration-auth/{integrationAuthId}/railway/environments": {
"get": {
"description": "",
"parameters": [
{
"name": "integrationAuthId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "appId",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v1/integration-auth/{integrationAuthId}/railway/services": {
"get": {
"description": "",
"parameters": [
{
"name": "integrationAuthId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "appId",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/signup/complete-account/signup": {
"post": {
"description": "",
@@ -2870,9 +2995,6 @@
}
}
}
},
"400": {
"description": "Bad Request"
}
},
"security": [
@@ -2882,6 +3004,26 @@
]
}
},
"/api/v2/organizations/{organizationId}/service-accounts": {
"get": {
"description": "",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/workspace/{workspaceId}/environments": {
"post": {
"description": "",
@@ -4018,9 +4160,6 @@
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
}
},
"requestBody": {
@@ -4073,6 +4212,138 @@
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/me": {
"get": {
"description": "",
"parameters": [],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}": {
"get": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
},
"delete": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/": {
"post": {
"description": "",
"parameters": [],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}/name": {
"patch": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"example": "any"
}
}
}
}
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}/permissions/workspace": {
"get": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
},
"post": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
@@ -4080,6 +4351,90 @@
"400": {
"description": "Bad Request"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environment": {
"example": "any"
},
"workspaceId": {
"example": "any"
},
"read": {
"example": "any"
},
"write": {
"example": "any"
},
"encryptedKey": {
"example": "any"
},
"nonce": {
"example": "any"
}
}
}
}
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}/permissions/workspace/{serviceAccountWorkspacePermissionId}": {
"delete": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "serviceAccountWorkspacePermissionId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}/keys": {
"get": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "workspaceId",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
@@ -4149,6 +4504,297 @@
}
}
},
"/api/v3/secrets/": {
"get": {
"description": "",
"parameters": [
{
"name": "workspaceId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "environment",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v3/secrets/{secretName}": {
"post": {
"description": "",
"parameters": [
{
"name": "secretName",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"example": "any"
},
"environment": {
"example": "any"
},
"type": {
"example": "any"
},
"secretKeyCiphertext": {
"example": "any"
},
"secretKeyIV": {
"example": "any"
},
"secretKeyTag": {
"example": "any"
},
"secretValueCiphertext": {
"example": "any"
},
"secretValueIV": {
"example": "any"
},
"secretValueTag": {
"example": "any"
},
"secretCommentCiphertext": {
"example": "any"
},
"secretCommentIV": {
"example": "any"
},
"secretCommentTag": {
"example": "any"
}
}
}
}
}
}
},
"get": {
"description": "",
"parameters": [
{
"name": "secretName",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "workspaceId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "environment",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "type",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
},
"patch": {
"description": "",
"parameters": [
{
"name": "secretName",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"example": "any"
},
"environment": {
"example": "any"
},
"type": {
"example": "any"
},
"secretValueCiphertext": {
"example": "any"
},
"secretValueIV": {
"example": "any"
},
"secretValueTag": {
"example": "any"
}
}
}
}
}
}
},
"delete": {
"description": "",
"parameters": [
{
"name": "secretName",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"example": "any"
},
"environment": {
"example": "any"
},
"type": {
"example": "any"
}
}
}
}
}
}
}
},
"/api/v3/workspaces/{workspaceId}/secrets/blind-index-status": {
"get": {
"description": "",
"parameters": [
{
"name": "workspaceId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v3/workspaces/{workspaceId}/secrets": {
"get": {
"description": "",
"parameters": [
{
"name": "workspaceId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v3/workspaces/{workspaceId}/secrets/names": {
"post": {
"description": "",
"parameters": [
{
"name": "workspaceId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretsToUpdate": {
"example": "any"
}
}
}
}
}
}
}
},
"/api/status": {
"get": {
"description": "",

View File

@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import { Bot, BotKey } from '../../models';
import { createBot } from '../../helpers/bot';
@@ -29,7 +30,7 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
// -> create a new bot and return it
bot = await createBot({
name: 'Infisical Bot',
workspaceId
workspaceId: new Types.ObjectId(workspaceId)
});
}
} catch (err) {

View File

@@ -56,7 +56,8 @@ export const createIntegration = async (req: Request, res: Response) => {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString()
workspaceId: integration.workspace,
environment: sourceEnvironment
})
});
}
@@ -117,7 +118,8 @@ export const updateIntegration = async (req: Request, res: Response) => {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString(),
workspaceId: integration.workspace,
environment
}),
});
}

View File

@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Key, Secret } from '../../models';
import {
v1PushSecrets as push,
@@ -84,7 +85,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});

View File

@@ -8,6 +8,8 @@ import { BadRequestError, InternalServerError, UnauthorizedRequestError, Validat
import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { TelemetryService } from '../../services';
import { User } from "../../models";
import { AccountNotFoundError } from '../../utils/errors';
/**
* Create secret for workspace with id [workspaceId] and environment [environment]
@@ -340,15 +342,18 @@ export const getSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
let userEmail: Types.ObjectId | undefined = undefined // used for posthog
let userEmail: string | undefined = undefined // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
userId = req.serviceTokenData.user;
const user = await User.findById(req.serviceTokenData.user, 'email');
if (!user) throw AccountNotFoundError();
userEmail = user.email;
}
const [err, secrets] = await to(Secret.find(

View File

@@ -2,7 +2,7 @@ import to from 'await-to-js';
import { Types } from 'mongoose';
import { Request, Response } from 'express';
import { ISecret, Secret } from '../../models';
import { IAction } from '../../ee/models';
import { IAction, SecretVersion } from '../../ee/models';
import {
SECRET_PERSONAL,
SECRET_SHARED,
@@ -15,7 +15,7 @@ import { UnauthorizedRequestError, ValidationError } from '../../utils/errors';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { EESecretService, EELogService } from '../../ee/services';
import { TelemetryService } from '../../services';
import { TelemetryService, SecretService } from '../../services';
import { getChannelFromUserAgent } from '../../utils/posthog';
import { PERMISSION_WRITE_SECRETS } from '../../variables';
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
@@ -33,6 +33,7 @@ import {
* @param res
*/
export const batchSecrets = async (req: Request, res: Response) => {
const channel = getChannelFromUserAgent(req.headers['user-agent']);
const postHogClient = TelemetryService.getPostHogClient();
@@ -50,29 +51,47 @@ export const batchSecrets = async (req: Request, res: Response) => {
const updateSecrets: BatchSecret[] = [];
const deleteSecrets: Types.ObjectId[] = [];
const actions: IAction[] = [];
// get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId)
});
requests.forEach((request) => {
for await (const request of requests) {
let secretBlindIndex = '';
switch (request.method) {
case 'POST':
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName: request.secret.secretName,
salt
});
createSecrets.push({
...request.secret,
version: 1,
user: request.secret.type === SECRET_PERSONAL ? req.user : undefined,
environment,
workspace: new Types.ObjectId(workspaceId)
workspace: new Types.ObjectId(workspaceId),
secretBlindIndex
});
break;
case 'PATCH':
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName: request.secret.secretName,
salt
});
updateSecrets.push({
...request.secret,
_id: new Types.ObjectId(request.secret._id)
_id: new Types.ObjectId(request.secret._id),
secretBlindIndex
});
break;
case 'DELETE':
deleteSecrets.push(new Types.ObjectId(request.secret._id));
break;
}
});
}
// handle create secrets
let createdSecrets: ISecret[] = [];
@@ -133,7 +152,10 @@ export const batchSecrets = async (req: Request, res: Response) => {
const updateOperations = updateSecrets.map((u) => ({
updateOne: {
filter: { _id: new Types.ObjectId(u._id) },
filter: {
_id: new Types.ObjectId(u._id),
workspace: new Types.ObjectId(workspaceId)
},
update: {
$inc: {
version: 1
@@ -146,13 +168,14 @@ export const batchSecrets = async (req: Request, res: Response) => {
await Secret.bulkWrite(updateOperations);
const secretVersions = updateSecrets.map((u) => ({
const secretVersions = updateSecrets.map((u) => new SecretVersion({
secret: new Types.ObjectId(u._id),
version: listedSecretsObj[u._id.toString()].version,
workspace: new Types.ObjectId(workspaceId),
type: listedSecretsObj[u._id.toString()].type,
environment,
isDeleted: false,
secretBlindIndex: u.secretBlindIndex,
secretKeyCiphertext: u.secretKeyCiphertext,
secretKeyIV: u.secretKeyIV,
secretKeyTag: u.secretKeyTag,
@@ -247,13 +270,13 @@ export const batchSecrets = async (req: Request, res: Response) => {
// // trigger event - push secrets
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId
workspaceId: new Types.ObjectId(workspaceId)
})
});
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
workspaceId: new Types.ObjectId(workspaceId)
});
const resObj: { [key: string]: ISecret[] | string[] } = {}
@@ -351,8 +374,14 @@ export const createSecrets = async (req: Request, res: Response) => {
listOfSecretsToCreate = [req.body.secrets];
}
// get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId)
});
type secretsToCreateType = {
type: string;
secretName?: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
@@ -365,25 +394,10 @@ export const createSecrets = async (req: Request, res: Response) => {
tags: string[]
}
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),
const secretsToInsert: ISecret[] = await Promise.all(
listOfSecretsToCreate.map(async ({
type,
user: (req.user && type === SECRET_PERSONAL) ? req.user : undefined,
environment,
secretName,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@@ -394,16 +408,43 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentIV,
secretCommentTag,
tags
});
});
}: secretsToCreateType) => {
let secretBlindIndex;
if (secretName) {
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName,
salt
});
}
return ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
type,
...(secretBlindIndex ? { secretBlindIndex } : {}),
user: (req.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
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId
workspaceId: new Types.ObjectId(workspaceId)
})
});
}, 5000);
@@ -417,35 +458,28 @@ export const createSecrets = async (req: Request, res: Response) => {
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
}) => ({
_id: new Types.ObjectId(),
secretValueTag
}) => new SecretVersion({
secret: _id,
version,
workspace,
type,
user,
environment,
secretBlindIndex,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
secretValueTag
}))
});
@@ -471,17 +505,15 @@ export const createSecrets = async (req: Request, res: Response) => {
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
workspaceId: new Types.ObjectId(workspaceId)
});
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
distinctId: await TelemetryService.getDistinctId({
authData: req.authData
}),
properties: {
numberOfSecrets: listOfSecretsToCreate.length,
@@ -595,7 +627,7 @@ export const getSecrets = async (req: Request, res: Response) => {
// case: client authorization is via service token
if (req.serviceTokenData) {
const userId = req.serviceTokenData.user._id
const userId = req.serviceTokenData.user;
const secretQuery: any = {
workspace: workspaceId,
@@ -655,10 +687,8 @@ export const getSecrets = async (req: Request, res: Response) => {
if (postHogClient) {
postHogClient.capture({
event: 'secrets pulled',
distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
distinctId: await TelemetryService.getDistinctId({
authData: req.authData
}),
properties: {
numberOfSecrets: secrets.length,
@@ -845,7 +875,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
setTimeout(async () => {
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: key
workspaceId: new Types.ObjectId(key)
})
});
}, 10000);
@@ -872,17 +902,15 @@ export const updateSecrets = async (req: Request, res: Response) => {
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: key
workspaceId: new Types.ObjectId(key)
})
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
distinctId: await TelemetryService.getDistinctId({
authData: req.authData
}),
properties: {
numberOfSecrets: workspaceSecretObj[key].length,
@@ -955,10 +983,6 @@ export const deleteSecrets = async (req: Request, res: Response) => {
}
*/
return res.status(200).send({
message: 'delete secrets!!'
});
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const toDelete = req.secrets.map((s: any) => s._id);
@@ -987,7 +1011,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
// trigger event - push secrets
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: key
workspaceId: new Types.ObjectId(key)
})
});
const deleteAction = await EELogService.createAction({
@@ -1012,17 +1036,15 @@ export const deleteSecrets = async (req: Request, res: Response) => {
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: key
})
workspaceId: new Types.ObjectId(key)
});
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
distinctId: await TelemetryService.getDistinctId({
authData: req.authData
}),
properties: {
numberOfSecrets: workspaceSecretObj[key].length,

View File

@@ -11,9 +11,11 @@ import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissi
import {
PERMISSION_READ_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN
} from '../../variables';
import { getSaltRounds } from '../../config';
import { BadRequestError } from '../../utils/errors';
/**
* Return service token data associated with service token on request
@@ -48,7 +50,16 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
}
*/
return res.status(200).json(req.serviceTokenData);
if (!(req.authData.authPayload instanceof ServiceTokenData)) throw BadRequestError({
message: 'Failed accepted client validation for service token data'
});
const serviceTokenData = await ServiceTokenData
.findById(req.authData.authPayload._id)
.select('+encryptedKey +iv +tag')
.populate('user');
return res.status(200).json(serviceTokenData);
}
/**
@@ -76,7 +87,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
const secretHash = await bcrypt.hash(secret, getSaltRounds());
let expiresAt;
if (!!expiresIn) {
if (expiresIn) {
expiresAt = new Date()
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}

View File

@@ -95,7 +95,8 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
@@ -131,7 +132,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
if (req.user) {
userId = req.user._id.toString();
} else if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userId = req.serviceTokenData.user.toString();
}
// validate environment
const workspaceEnvs = req.membership.workspace.environments;

View File

@@ -0,0 +1,7 @@
import * as secretsController from './secretsController';
import * as workspacesController from './workspacesController';
export {
secretsController,
workspacesController
}

View File

@@ -0,0 +1,183 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import {
SecretService,
TelemetryService,
EventService
} from '../../services';
import { eventPushSecrets } from '../../events';
import { getAuthDataPayloadIdObj } from '../../utils/auth';
import { BadRequestError } from '../../utils/errors';
/**
* Get secrets for workspace with id [workspaceId] and environment
* [environment]
* @param req
* @param res
*/
export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
authData: req.authData
});
return res.status(200).send({
secrets
});
}
/**
* Get secret with name [secretName]
* @param req
* @param res
*/
export const getSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const type = req.query.type as 'shared' | 'personal' | undefined;
const secret = await SecretService.getSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData
});
return res.status(200).send({
secret
});
}
/**
* Create secret with name [secretName]
* @param req
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
} = req.body;
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
...((secretCommentCiphertext && secretCommentIV && secretCommentTag) ? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
} : {})
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({
secret: secretWithoutBlindIndex
});
}
/**
* Update secret with name [secretName]
* @param req
* @param res
*/
export const updateSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretValueCiphertext,
secretValueIV,
secretValueTag
} = req.body;
const secret = await SecretService.updateSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData,
secretValueCiphertext,
secretValueIV,
secretValueTag
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
return res.status(200).send({
secret
});
}
/**
* Delete secret with name [secretName]
* @param req
* @param res
*/
export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type
} = req.body;
const { secret, secrets } = await SecretService.deleteSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
return res.status(200).send({
secret
});
}

View File

@@ -0,0 +1,90 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import { Secret } from '../../models';
import { SecretService } from'../../services';
/**
* Return whether or not all secrets in workspace with id [workspaceId]
* are blind-indexed
* @param req
* @param res
* @returns
*/
export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const secretsWithoutBlindIndex = await Secret.countDocuments({
workspace: new Types.ObjectId(workspaceId),
secretBlindIndex: {
$exists: false
}
});
return res.status(200).send(secretsWithoutBlindIndex === 0);
}
/**
* Get all secrets for workspace with id [workspaceId]
*/
export const getWorkspaceSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const secrets = await Secret.find({
workspace: new Types.ObjectId (workspaceId)
});
return res.status(200).send({
secrets
});
}
/**
* Update blind indices for secrets in workspace with id [workspaceId]
* @param req
* @param res
*/
export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
interface SecretToUpdate {
secretName: string;
_id: string;
}
const { workspaceId } = req.params;
const {
secretsToUpdate
}: {
secretsToUpdate: SecretToUpdate[];
} = req.body;
// get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId)
});
// update secret blind indices
const operations = await Promise.all(
secretsToUpdate.map(async (secretToUpdate: SecretToUpdate) => {
const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName: secretToUpdate.secretName,
salt
});
return ({
updateOne: {
filter: {
_id: new Types.ObjectId(secretToUpdate._id)
},
update: {
secretBlindIndex
}
}
});
})
);
await Secret.bulkWrite(operations);
return res.status(200).send({
message: 'Successfully named workspace secrets'
});
}

View File

@@ -146,7 +146,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
const oldSecretVersion = await SecretVersion.findOne({
secret: secretId,
version
});
}).select('+secretBlindIndex')
if (!oldSecretVersion) throw new Error('Failed to find secret version');
@@ -155,6 +155,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@@ -174,6 +175,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
type,
user,
environment,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@@ -197,6 +199,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
user,
environment,
isDeleted: false,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@@ -207,7 +210,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: secret.workspace.toString()
workspaceId: secret.workspace
});
} catch (err) {

View File

@@ -15,16 +15,10 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
secretSnapshot = await SecretSnapshot
.findById(secretSnapshotId)
.populate({
path: 'secretVersions',
populate: {
path: 'tags',
model: 'Tag',
}
});
.populate('secretVersions');
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);

View File

@@ -173,6 +173,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
}
}
*/
let secrets;
try {
const { workspaceId } = req.params;
@@ -182,7 +183,10 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
const secretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId,
version
}).populate<{ secretVersions: ISecretVersion[]}>('secretVersions');
}).populate<{ secretVersions: ISecretVersion[]}>({
path: 'secretVersions',
select: '+secretBlindIndex'
});
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
@@ -222,6 +226,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@@ -240,6 +245,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
type,
user,
environment,
secretBlindIndex: secretBlindIndex ?? undefined,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@@ -257,7 +263,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
);
// add secret versions
await SecretVersion.insertMany(
const secretV = await SecretVersion.insertMany(
secrets.map(({
_id,
version,
@@ -265,6 +271,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@@ -282,6 +289,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
user,
environment,
isDeleted: false,
secretBlindIndex: secretBlindIndex ?? undefined,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@@ -304,7 +312,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
workspaceId: new Types.ObjectId(workspaceId)
});
} catch (err) {
Sentry.setUser({ email: req.user.email });

View File

@@ -4,6 +4,7 @@ import {
Log,
IAction
} from '../models';
/**
* Create an (audit) log
* @param {Object} obj

View File

@@ -2,7 +2,7 @@ import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
Secret,
ISecret
ISecret,
} from '../../models';
import {
SecretSnapshot,
@@ -21,7 +21,7 @@ import {
const takeSecretSnapshotHelper = async ({
workspaceId
}: {
workspaceId: string;
workspaceId: Types.ObjectId;
}) => {
let secretSnapshot;
@@ -143,7 +143,7 @@ const initSecretVersioningHelper = async () => {
if (unversionedSecrets.length > 0) {
await addSecretVersionsHelper({
secretVersions: unversionedSecrets.map((s, idx) => ({
secretVersions: unversionedSecrets.map((s, idx) => new SecretVersion({
...s,
secret: s._id,
version: s.version ? s.version : 1,

View File

@@ -5,6 +5,7 @@ import {
} from '../../variables';
export interface ISecretVersion {
_id: Types.ObjectId;
secret: Types.ObjectId;
version: number;
workspace: Types.ObjectId; // new
@@ -12,13 +13,13 @@ export interface ISecretVersion {
user?: Types.ObjectId; // new
environment: string; // new
isDeleted: boolean;
secretBlindIndex?: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
tags?: string[];
}
const secretVersionSchema = new Schema<ISecretVersion>(
@@ -57,6 +58,10 @@ const secretVersionSchema = new Schema<ISecretVersion>(
default: false,
required: true
},
secretBlindIndex: {
type: String,
select: false
},
secretKeyCiphertext: {
type: String,
required: true
@@ -80,12 +85,7 @@ const secretVersionSchema = new Schema<ISecretVersion>(
secretValueTag: {
type: String, // symmetric
required: true
},
tags: {
ref: 'Tag',
type: [Schema.Types.ObjectId],
default: []
},
}
},
{
timestamps: true

View File

@@ -25,7 +25,7 @@ class EESecretService {
static async takeSecretSnapshot({
workspaceId
}: {
workspaceId: string;
workspaceId: Types.ObjectId;
}) {
if (!EELicenseService.isLicenseValid) return;
return await takeSecretSnapshotHelper({ workspaceId });

View File

@@ -1,3 +1,4 @@
import { Types } from 'mongoose';
import {
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS
@@ -22,13 +23,16 @@ interface PushSecret {
* @returns
*/
const eventPushSecrets = ({
workspaceId
workspaceId,
environment
}: {
workspaceId: string;
workspaceId: Types.ObjectId;
environment?: string;
}) => {
return ({
name: EVENT_PUSH_SECRETS,
workspaceId,
environment,
payload: {
}

View File

@@ -157,7 +157,7 @@ const getAuthSTDPayload = async ({
}, {
new: true
})
.select('+encryptedKey +iv +tag').populate('user serviceAccount');
.select('+encryptedKey +iv +tag');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });

View File

@@ -112,7 +112,7 @@ const createBot = async ({
workspaceId,
}: {
name: string;
workspaceId: string;
workspaceId: Types.ObjectId;
}) => {
let bot;
try {
@@ -151,7 +151,7 @@ const getSecretsHelper = async ({
workspaceId,
environment
}: {
workspaceId: string;
workspaceId: Types.ObjectId;
environment: string;
}) => {
const content = {} as any;
@@ -196,7 +196,7 @@ const getSecretsHelper = async ({
* @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key
*/
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => {
let key;
try {
const botKey = await BotKey.findOne({
@@ -245,7 +245,7 @@ const encryptSymmetricHelper = async ({
workspaceId,
plaintext
}: {
workspaceId: string;
workspaceId: Types.ObjectId;
plaintext: string;
}) => {
@@ -282,7 +282,7 @@ const decryptSymmetricHelper = async ({
iv,
tag
}: {
workspaceId: string;
workspaceId: Types.ObjectId;
ciphertext: string;
iv: string;
tag: string;

View File

@@ -1,5 +1,6 @@
import mongoose from 'mongoose';
import { EESecretService } from '../ee/services';
import { SecretService } from '../services';
import { getLogger } from '../utils/logger';
/**
@@ -22,6 +23,7 @@ const initDatabaseHelper = async ({
getLogger("database").info("Database connection established");
await EESecretService.initSecretVersioning();
await SecretService.initSecretBlindIndexDataHelper();
} catch (err) {
getLogger("database").error(`Unable to establish Database connection due to the error.\n${err}`);
}

View File

@@ -1,11 +1,13 @@
import { Bot, IBot } from '../models';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import { Bot, IBot } from '../models';
import { EVENT_PUSH_SECRETS } from '../variables';
import { IntegrationService } from '../services';
interface Event {
name: string;
workspaceId: string;
workspaceId: Types.ObjectId;
environment?: string;
payload: any;
}
@@ -22,7 +24,10 @@ const handleEventHelper = async ({
}: {
event: Event;
}) => {
const { workspaceId } = event;
const {
workspaceId,
environment
} = event;
// TODO: moduralize bot check into separate function
const bot = await Bot.findOne({
@@ -36,7 +41,8 @@ const handleEventHelper = async ({
switch (event.name) {
case EVENT_PUSH_SECRETS:
IntegrationService.syncIntegrations({
workspaceId
workspaceId,
environment
});
break;
}

View File

@@ -217,14 +217,19 @@ const handleOAuthExchangeHelper = async ({
* @param {Object} obj.workspaceId - id of workspace
*/
const syncIntegrationsHelper = async ({
workspaceId
workspaceId,
environment
}: {
workspaceId: string;
workspaceId: Types.ObjectId;
environment?: string;
}) => {
let integrations;
try {
integrations = await Integration.find({
workspace: workspaceId,
...(environment ? {
environment
} : {}),
isActive: true,
app: { $ne: null }
});
@@ -234,7 +239,7 @@ const syncIntegrationsHelper = async ({
for await (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({ // issue here?
workspaceId: integration.workspace.toString(),
workspaceId: integration.workspace,
environment: integration.environment
});
@@ -281,7 +286,7 @@ const syncIntegrationsHelper = async ({
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string
@@ -318,7 +323,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string
@@ -340,7 +345,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
accessId = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessIdCiphertext as string,
iv: integrationAuth.accessIdIV as string,
tag: integrationAuth.accessIdTag as string
@@ -386,7 +391,7 @@ const setIntegrationAuthRefreshHelper = async ({
if (!integrationAuth) throw new Error('Failed to find integration auth');
const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
workspaceId: integrationAuth.workspace,
plaintext: refreshToken
});
@@ -435,14 +440,14 @@ const setIntegrationAuthAccessHelper = async ({
if (!integrationAuth) throw new Error('Failed to find integration auth');
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
workspaceId: integrationAuth.workspace,
plaintext: accessToken
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
workspaceId: integrationAuth.workspace,
plaintext: accessId
});
}

View File

@@ -10,7 +10,8 @@ import {
EELogService
} from '../ee/services';
import {
IAction
IAction,
SecretVersion
} from '../ee/models';
import {
SECRET_SHARED,
@@ -189,8 +190,7 @@ const v1PushSecrets = async ({
secretKeyHash,
}) => {
const newSecret = newSecretsObj[`${type}-${secretKeyHash}`];
return ({
_id: new Types.ObjectId(),
return new SecretVersion({
secret: _id,
version: version ? version + 1 : 1,
workspace: new Types.ObjectId(workspaceId),
@@ -261,8 +261,7 @@ const v1PushSecrets = async ({
secretValueIV,
secretValueTag,
secretValueHash
}) => ({
_id: new Types.ObjectId(),
}) => new SecretVersion({
secret: _id,
version,
workspace,
@@ -284,7 +283,7 @@ const v1PushSecrets = async ({
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
workspaceId: new Types.ObjectId(workspaceId)
});
} catch (err) {
Sentry.setUser(null);
@@ -475,12 +474,12 @@ const v2PushSecrets = async ({
// (EE) add secret versions for new secrets
EESecretService.addSecretVersions({
secretVersions: newSecrets.map((secretDocument) => {
return {
...secretDocument.toObject(),
secretVersions: newSecrets.map((secretDocument: ISecret) => {
return new SecretVersion({
...secretDocument,
secret: secretDocument._id,
isDeleted: false
}
})
})
});
@@ -495,7 +494,7 @@ const v2PushSecrets = async ({
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
workspaceId: new Types.ObjectId(workspaceId)
})
// (EE) create (audit) log

View File

@@ -1,14 +1,24 @@
import { Types } from 'mongoose';
import {
CreateSecretParams,
GetSecretsParams,
GetSecretParams,
UpdateSecretParams,
DeleteSecretParams
} from '../interfaces/services/SecretService';
import {
AuthData
} from '../interfaces/middleware';
import {
User,
IUser,
Workspace,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
Secret,
ISecret
ISecret,
SecretBlindIndexData,
} from '../models';
import { SecretVersion } from '../ee/models';
import {
validateMembership
} from '../helpers/membership';
@@ -17,7 +27,8 @@ import {
validateUserClientForSecrets
} from '../helpers/user';
import {
validateServiceTokenDataClientForSecrets, validateServiceTokenDataClientForWorkspace
validateServiceTokenDataClientForSecrets,
validateServiceTokenDataClientForWorkspace
} from '../helpers/serviceTokenData';
import {
validateServiceAccountClientForSecrets,
@@ -26,14 +37,37 @@ import {
import {
BadRequestError,
UnauthorizedRequestError,
SecretNotFoundError
SecretNotFoundError,
SecretBlindIndexDataNotFoundError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
AUTH_MODE_API_KEY,
SECRET_PERSONAL,
SECRET_SHARED,
ACTION_ADD_SECRETS,
ACTION_READ_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS
} from '../variables';
import crypto from 'crypto';
import * as argon2 from 'argon2';
import {
encryptSymmetric,
decryptSymmetric
} from '../utils/crypto';
import { getEncryptionKey } from '../config';
import { TelemetryService } from '../services';
import {
EESecretService,
EELogService
} from '../ee/services';
import {
getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj
} from '../utils/auth';
/**
* Validate authenticated clients for secrets with id [secretId] based
@@ -50,10 +84,7 @@ const validateClientForSecret = async ({
acceptedRoles,
requiredPermissions
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
authData: AuthData;
secretId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions: string[];
@@ -127,10 +158,7 @@ const validateClientForSecrets = async ({
secretIds,
requiredPermissions
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
authData: AuthData;
secretIds: Types.ObjectId[];
requiredPermissions: string[];
}) => {
@@ -192,7 +220,726 @@ const validateClientForSecrets = async ({
});
}
/**
* Initialize secret blind index data by setting previously
* un-initialized projects to have secret blind index data
* (Ensures that all projects have associated blind index data)
*/
const initSecretBlindIndexDataHelper = async () => {
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct('workspace');
const workspaceIdsToBlindIndex = await Workspace.distinct('_id', {
_id: {
$nin: workspaceIdsBlindIndexed
}
});
const secretBlindIndexDataToInsert = workspaceIdsToBlindIndex.map((workspaceToBlindIndex) => {
const salt = crypto.randomBytes(16).toString('base64');
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = encryptSymmetric({
plaintext: salt,
key: getEncryptionKey()
});
const secretBlindIndexData = new SecretBlindIndexData({
workspace: workspaceToBlindIndex,
encryptedSaltCiphertext,
saltIV,
saltTag
})
return secretBlindIndexData;
});
if (secretBlindIndexDataToInsert.length > 0) {
await SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert);
}
}
/**
* Create secret blind index data containing encrypted blind index [salt]
* for workspace with id [workspaceId]
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId
*/
const createSecretBlindIndexDataHelper = async ({
workspaceId
}: {
workspaceId: Types.ObjectId;
}) => {
// initialize random blind index salt for workspace
const salt = crypto.randomBytes(16).toString('base64');
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = encryptSymmetric({
plaintext: salt,
key: getEncryptionKey()
});
const secretBlindIndexData = await new SecretBlindIndexData({
workspace: workspaceId,
encryptedSaltCiphertext,
saltIV,
saltTag
}).save();
return secretBlindIndexData;
}
/**
* Get secret blind index salt for workspace with id [workspaceId]
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
* @returns
*/
const getSecretBlindIndexSaltHelper = async ({
workspaceId
}: {
workspaceId: Types.ObjectId;
}) => {
// check if workspace blind index data exists
const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId
});
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
// decrypt workspace salt
const salt = decryptSymmetric({
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV,
tag: secretBlindIndexData.saltTag,
key: getEncryptionKey()
});
return salt;
}
/**
* Generate blind index for secret with name [secretName]
* and salt [salt]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to generate blind index for
* @param {String} obj.salt - base64-salt
*/
const generateSecretBlindIndexWithSaltHelper = async ({
secretName,
salt
}: {
secretName: string;
salt: string;
}) => {
// generate secret blind index
const secretBlindIndex = (await argon2.hash(secretName, {
type: argon2.argon2id,
salt: Buffer.from(salt, 'base64'),
saltLength: 16, // default 16 bytes
memoryCost: 65536, // default pool of 64 MiB per thread.
hashLength: 32,
parallelism: 1,
raw: true
})).toString('base64');
return secretBlindIndex;
}
/**
* Generate blind index for secret with name [secretName]
* for workspace with id [workspaceId]
* @param {Object} obj
* @param {Stringj} obj.secretName - name of secret to generate blind index for
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
*/
const generateSecretBlindIndexHelper = async ({
secretName,
workspaceId
}: {
secretName: string;
workspaceId: Types.ObjectId;
}) => {
// check if workspace blind index data exists
const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId
});
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
// decrypt workspace salt
const salt = decryptSymmetric({
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV,
tag: secretBlindIndexData.saltTag,
key: getEncryptionKey()
});
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
secretName,
salt
});
return secretBlindIndex;
}
/**
* Create secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to create
* @param {Types.ObjectId} obj.workspaceId - id of workspace to create secret for
* @param {String} obj.environment - environment in workspace to create secret for
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const createSecretHelper = async ({
secretName,
workspaceId,
environment,
type,
authData,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
}: CreateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
});
const exists = await Secret.exists({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
});
if (exists) throw BadRequestError({
message: 'Failed to create secret that already exists'
});
if (type === SECRET_PERSONAL) {
// case: secret type is personal -> check if a corresponding shared secret
// with the same blind index [secretBlindIndex] exists
const exists = await Secret.exists({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
type: SECRET_SHARED
});
if (!exists) throw BadRequestError({
message: 'Failed to create personal secret override for no corresponding shared secret'
});
}
// create secret
const secret = await new Secret({
version: 1,
workspace: new Types.ObjectId(workspaceId),
environment,
type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
}).save();
const secretVersion = new SecretVersion({
secret: secret._id,
version: secret.version,
workspace: secret.workspace,
type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment,
isDeleted: false,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
});
// // (EE) add version for new secret
await EESecretService.addSecretVersions({
secretVersions: [secretVersion]
});
// (EE) create (audit) log
const action = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
...getAuthDataPayloadIdObj(authData),
workspaceId,
secretIds: [secret._id]
});
action && await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
});
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
});
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: 1,
environment,
workspaceId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
}
});
}
return secret;
}
/**
* Get secrets for workspace with id [workspaceId] and environment [environment]
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment in workspace
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const getSecretsHelper = async ({
workspaceId,
environment,
authData
}: GetSecretsParams) => {
let secrets: ISecret[] = [];
// get personal secrets first
secrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
environment,
type: SECRET_PERSONAL,
...getAuthDataPayloadUserObj(authData)
});
// concat with shared secrets
secrets = secrets.concat(await Secret.find({
workspace: new Types.ObjectId(workspaceId),
environment,
type: SECRET_SHARED,
secretBlindIndex: {
$nin: secrets.map((secret) => secret.secretBlindIndex)
}
}));
// (EE) create (audit) log
const action = await EELogService.createAction({
name: ACTION_READ_SECRETS,
...getAuthDataPayloadIdObj(authData),
workspaceId,
secretIds: secrets.map((secret) => secret._id)
});
action && await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
});
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets pulled',
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
}
});
}
return secrets;
}
/**
* Get secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to get
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const getSecretHelper = async ({
secretName,
workspaceId,
environment,
type,
authData
}: GetSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
});
let secret: ISecret | null = null;
// try getting personal secret first (if exists)
secret = await Secret.findOne({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
type: type ?? SECRET_PERSONAL,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
});
if (!secret) {
// case: failed to find personal secret matching criteria
// -> find shared secret matching criteria
secret = await Secret.findOne({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
type: SECRET_SHARED
});
}
if (!secret) throw SecretNotFoundError();
// (EE) create (audit) log
const action = await EELogService.createAction({
name: ACTION_READ_SECRETS,
...getAuthDataPayloadIdObj(authData),
workspaceId,
secretIds: [secret._id]
});
action && await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
});
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets pull',
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: 1,
environment,
workspaceId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
}
});
}
return secret;
}
/**
* Update secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to update
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {String} obj.secretValueCiphertext - ciphertext of secret value
* @param {String} obj.secretValueIV - IV of secret value
* @param {String} obj.secretValueTag - tag of secret value
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const updateSecretHelper = async ({
secretName,
workspaceId,
environment,
type,
authData,
secretValueCiphertext,
secretValueIV,
secretValueTag
}: UpdateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
});
let secret: ISecret | null = null;
if (type === SECRET_SHARED) {
// case: update shared secret
secret = await Secret.findOneAndUpdate(
{
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
type
},
{
secretValueCiphertext,
secretValueIV,
secretValueTag,
$inc: { version: 1 }
},
{
new: true
}
);
} else {
// case: update personal secret
secret = await Secret.findOneAndUpdate(
{
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
type,
...getAuthDataPayloadUserObj(authData)
},
{
secretValueCiphertext,
secretValueIV,
secretValueTag,
$inc: { version: 1 }
},
{
new: true
}
);
}
if (!secret) throw SecretNotFoundError();
const secretVersion = new SecretVersion({
secret: secret._id,
version: secret.version,
workspace: secret.workspace,
type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment,
isDeleted: false,
secretBlindIndex,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
});
// (EE) add version for new secret
await EESecretService.addSecretVersions({
secretVersions: [secretVersion]
});
// (EE) create (audit) log
const action = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS,
...getAuthDataPayloadIdObj(authData),
workspaceId,
secretIds: [secret._id]
});
action && await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
});
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
});
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: 1,
environment,
workspaceId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
}
});
}
return secret;
}
/**
* Delete secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to delete
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const deleteSecretHelper = async ({
secretName,
workspaceId,
environment,
type,
authData
}: DeleteSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
});
let secrets: ISecret[] = [];
let secret: ISecret | null = null;
if (type === SECRET_SHARED) {
secrets = await Secret.find({
secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId),
environment
});
secret = await Secret.findOneAndDelete({
secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type
});
await Secret.deleteMany({
secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId),
environment
});
} else {
secret = await Secret.findOneAndDelete({
secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
...getAuthDataPayloadUserObj(authData)
});
if (secret) {
secrets = [secret];
}
}
if (!secret) throw SecretNotFoundError();
await EESecretService.markDeletedSecretVersions({
secretIds: secrets.map((secret) => secret._id)
});
// (EE) create (audit) log
const action = await EELogService.createAction({
name: ACTION_DELETE_SECRETS,
...getAuthDataPayloadIdObj(authData),
workspaceId,
secretIds: secrets.map((secret) => secret._id)
});
// (EE) take a secret snapshot
action && await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
});
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
});
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
}
});
}
return ({
secrets,
secret
});
}
export {
validateClientForSecret,
validateClientForSecrets
validateClientForSecrets,
initSecretBlindIndexDataHelper,
createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper,
generateSecretBlindIndexHelper,
createSecretHelper,
getSecretsHelper,
getSecretHelper,
updateSecretHelper,
deleteSecretHelper
}

View File

@@ -111,7 +111,6 @@ const validateClientForServiceTokenData = async ({
environment?: string;
requiredPermissions?: string[];
}) => {
if (!serviceTokenData.workspace.equals(workspaceId)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({

View File

@@ -1,4 +1,5 @@
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import { Types } from 'mongoose';
import {
Workspace,
@@ -13,6 +14,7 @@ import {
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
SecretBlindIndexData
} from '../models';
import { createBot } from '../helpers/bot';
import { validateUserClientForWorkspace } from '../helpers/user';
@@ -26,6 +28,9 @@ import {
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import { getEncryptionKey } from '../config';
import { encryptSymmetric } from '../utils/crypto';
import { SecretService } from '../services';
/**
* Validate authenticated clients for workspace with id [workspaceId] based
@@ -42,7 +47,8 @@ const validateClientForWorkspace = async ({
workspaceId,
environment,
acceptedRoles,
requiredPermissions
requiredPermissions,
requireBlindIndicesEnabled
}: {
authData: {
authMode: string;
@@ -52,6 +58,7 @@ const validateClientForWorkspace = async ({
environment?: string;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions?: string[];
requireBlindIndicesEnabled: boolean;
}) => {
const workspace = await Workspace.findById(workspaceId);
@@ -60,6 +67,20 @@ const validateClientForWorkspace = async ({
message: 'Failed to find workspace'
});
if (requireBlindIndicesEnabled) {
// case: blind indices are not enabled for secrets in this workspace
// (i.e. workspace was created before blind indices were introduced
// and no admin has enabled it)
const secretBlindIndexData = await SecretBlindIndexData.exists({
workspace: new Types.ObjectId(workspaceId)
});
if (!secretBlindIndexData) throw UnauthorizedRequestError({
message: 'Failed workspace authorization due to blind indices not being enabled'
});
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
user: authData.authPayload,
@@ -130,13 +151,21 @@ const createWorkspace = async ({
// create workspace
workspace = await new Workspace({
name,
organization: organizationId
organization: organizationId,
autoCapitalization: true
}).save();
const bot = await createBot({
// initialize bot for workspace
await createBot({
name: 'Infisical Bot',
workspaceId: workspace._id.toString()
workspaceId: workspace._id
});
// initialize blind index salt for workspace
await SecretService.createSecretBlindIndexData({
workspaceId: workspace._id
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);

View File

@@ -61,6 +61,10 @@ import {
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from './routes/v2';
import {
secrets as v3SecretsRouter,
workspaces as v3WorkspacesRouter
} from './routes/v3';
import { healthCheck } from './routes/status';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
@@ -122,7 +126,7 @@ const main = async () => {
app.use('/api/v1/workspace', eeWorkspaceRouter);
app.use('/api/v1/action', eeActionRouter);
// v1 routes
// v1 routes (default)
app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/bot', v1BotRouter);
@@ -141,7 +145,7 @@ const main = async () => {
app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
// v2 routes
// v2 routes (improvements)
app.use('/api/v2/signup', v2SignupRouter);
app.use('/api/v2/auth', v2AuthRouter);
app.use('/api/v2/users', v2UsersRouter);
@@ -154,6 +158,10 @@ const main = async () => {
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new
app.use('/api/v2/api-key', v2APIKeyDataRouter);
// v3 routes (experimental)
app.use('/api/v3/secrets', v3SecretsRouter);
app.use('/api/v3/workspaces', v3WorkspacesRouter);
// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))

View File

@@ -0,0 +1,13 @@
import {
IUser,
IServiceAccount,
IServiceTokenData
} from '../../models';
export interface AuthData {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
authChannel: string;
authIP: string;
authUserAgent: string;
}

View File

@@ -0,0 +1,52 @@
import { Types } from 'mongoose';
import { AuthData } from '../../middleware';
export interface CreateSecretParams {
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type: 'shared' | 'personal';
authData: AuthData;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
}
export interface GetSecretsParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
}
export interface GetSecretParams {
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type?: 'shared' | 'personal';
authData: AuthData;
}
export interface UpdateSecretParams {
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type: 'shared' | 'personal',
authData: AuthData
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
}
export interface DeleteSecretParams {
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type: 'shared' | 'personal';
authData: AuthData;
}

View File

@@ -21,6 +21,7 @@ import {
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import { getChannelFromUserAgent } from '../utils/posthog';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -88,7 +89,10 @@ const requireAuth = ({
req.authData = {
authMode,
authPayload // User, ServiceAccount, ServiceTokenData
authPayload, // User, ServiceAccount, ServiceTokenData
authChannel: getChannelFromUserAgent(req.headers['user-agent']),
authIP: req.ip,
authUserAgent: req.headers['user-agent'] ?? 'other'
}
return next();

View File

@@ -17,12 +17,14 @@ const requireWorkspaceAuth = ({
acceptedRoles,
locationWorkspaceId,
locationEnvironment = undefined,
requiredPermissions = []
requiredPermissions = [],
requireBlindIndicesEnabled = false
}: {
acceptedRoles: Array<'admin' | 'member'>;
locationWorkspaceId: req;
locationEnvironment?: req | undefined;
requiredPermissions?: string[];
requireBlindIndicesEnabled?: boolean;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const workspaceId = req[locationWorkspaceId]?.workspaceId;
@@ -34,7 +36,8 @@ const requireWorkspaceAuth = ({
workspaceId: new Types.ObjectId(workspaceId),
environment,
acceptedRoles,
requiredPermissions
requiredPermissions,
requireBlindIndicesEnabled
});
if (membership) {

View File

@@ -9,6 +9,7 @@ import Membership, { IMembership } from './membership';
import MembershipOrg, { IMembershipOrg } from './membershipOrg';
import Organization, { IOrganization } from './organization';
import Secret, { ISecret } from './secret';
import SecretBlindIndexData, { ISecretBlindIndexData } from './secretBlindIndexData';
import ServiceToken, { IServiceToken } from './serviceToken';
import ServiceAccount, { IServiceAccount } from './serviceAccount'; // new
import ServiceAccountKey, { IServiceAccountKey } from './serviceAccountKey'; // new
@@ -45,6 +46,8 @@ export {
IOrganization,
Secret,
ISecret,
SecretBlindIndexData,
ISecretBlindIndexData,
ServiceToken,
IServiceToken,
ServiceAccount,

View File

@@ -11,6 +11,7 @@ export interface ISecret {
type: string;
user: Types.ObjectId;
environment: string;
secretBlindIndex?: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
@@ -57,6 +58,10 @@ const secretSchema = new Schema<ISecret>(
type: String,
required: true
},
secretBlindIndex: {
type: String,
select: false
},
secretKeyCiphertext: {
type: String,
required: true

View File

@@ -0,0 +1,35 @@
import { Schema, model, Types, Document } from 'mongoose';
export interface ISecretBlindIndexData extends Document {
_id: Types.ObjectId;
workspace: Types.ObjectId;
encryptedSaltCiphertext: string;
saltIV: string;
saltTag: string;
}
const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
encryptedSaltCiphertext: {
type: String,
required: true
},
saltIV: {
type: String,
required: true
},
saltTag: {
type: String,
required: true
}
}
);
const SecretBlindIndexData = model<ISecretBlindIndexData>('SecretBlindIndexData', secretBlindIndexDataSchema);
export default SecretBlindIndexData;

View File

@@ -33,7 +33,8 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
},
user: {
type: Schema.Types.ObjectId,
ref: 'User'
ref: 'User',
required: true
},
serviceAccount: {
type: Schema.Types.ObjectId,

View File

@@ -55,4 +55,4 @@ const workspaceSchema = new Schema<IWorkspace>({
const Workspace = model<IWorkspace>('Workspace', workspaceSchema);
export default Workspace;
export default Workspace;

View File

@@ -0,0 +1,7 @@
import secrets from './secrets';
import workspaces from './workspaces';
export {
secrets,
workspaces
}

View File

@@ -1,8 +1,157 @@
import express from 'express';
const router = express.Router();
import {
requireAuth, validateRequest
requireAuth,
requireWorkspaceAuth,
validateRequest
} from '../../middleware';
import { body } from 'express-validator';
import { body, param, query } from 'express-validator';
import { secretsController } from '../../controllers/v3';
import {
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
ADMIN,
MEMBER,
PERMISSION_WRITE_SECRETS,
SECRET_SHARED,
SECRET_PERSONAL,
PERMISSION_READ_SECRETS
} from '../../variables';
router.get(
'/',
query('workspaceId').exists().isString().trim(),
query('environment').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'query',
locationEnvironment: 'query',
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.getSecrets
);
router.post(
'/:secretName',
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
body('secretKeyCiphertext').exists().isString().trim(),
body('secretKeyIV').exists().isString().trim(),
body('secretKeyTag').exists().isString().trim(),
body('secretValueCiphertext').exists().isString().trim(),
body('secretValueIV').exists().isString().trim(),
body('secretValueTag').exists().isString().trim(),
body('secretCommentCiphertext').optional().isString().trim(),
body('secretCommentIV').optional().isString().trim(),
body('secretCommentTag').optional().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body',
locationEnvironment: 'body',
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.createSecret
);
router.get(
'/:secretName',
param('secretName').exists().isString().trim(),
query('workspaceId').exists().isString().trim(),
query('environment').exists().isString().trim(),
query('type').optional().isIn([SECRET_SHARED, SECRET_PERSONAL]),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'query',
locationEnvironment: 'query',
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.getSecretByName
);
router.patch(
'/:secretName',
param('secretName').exists().isString().trim(),
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
body('secretValueCiphertext').exists().isString().trim(),
body('secretValueIV').exists().isString().trim(),
body('secretValueTag').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body',
locationEnvironment: 'body',
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.updateSecretByName
);
router.delete(
'/:secretName',
param('secretName').exists().isString().trim(),
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('type').exists().isIn([SECRET_SHARED, SECRET_PERSONAL]),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body',
locationEnvironment: 'body',
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
}),
secretsController.deleteSecretByName
);
export default router;

View File

@@ -0,0 +1,80 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
validateRequest
} from '../../middleware';
import { workspacesController } from '../../controllers/v3';
import {
AUTH_MODE_JWT,
ADMIN,
PERMISSION_READ_SECRETS
} from '../../variables';
import { param, body, validationResult } from 'express-validator';
// -- migration to blind indices endpoints
router.get(
'/:workspaceId/secrets/blind-index-status',
param('workspaceId').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: 'params',
}),
workspacesController.getWorkspaceBlindIndexStatus
);
router.get( // allow admins to get all workspace secrets (part of blind indices migration)
'/:workspaceId/secrets',
param('workspaceId').exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: 'params',
}),
workspacesController.getWorkspaceSecrets
);
router.post( // allow admins to name all workspace secrets (part of blind indices migration)
'/:workspaceId/secrets/names',
param('workspaceId').exists().isString().trim(),
body('secretsToUpdate')
.exists()
.isArray()
.withMessage('secretsToUpdate must be an array')
.customSanitizer((value) => {
return value.map((secret: any) => ({
secretName: secret.secretName,
_id: secret._id
}));
}),
body('secretsToUpdate.*.secretName')
.exists()
.isString()
.withMessage('secretName must be a string'),
body('secretsToUpdate.*._id')
.exists()
.isString()
.withMessage('secretId must be a string'),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: 'params'
}),
workspacesController.nameWorkspaceSecrets
);
// --
export default router;

View File

@@ -1,3 +1,4 @@
import { Types } from 'mongoose';
import {
getSecretsHelper,
encryptSymmetricHelper,
@@ -21,7 +22,7 @@ class BotService {
workspaceId,
environment
}: {
workspaceId: string;
workspaceId: Types.ObjectId;
environment: string;
}) {
return await getSecretsHelper({
@@ -41,7 +42,7 @@ class BotService {
workspaceId,
plaintext
}: {
workspaceId: string;
workspaceId: Types.ObjectId;
plaintext: string;
}) {
return await encryptSymmetricHelper({
@@ -65,7 +66,7 @@ class BotService {
iv,
tag
}: {
workspaceId: string;
workspaceId: Types.ObjectId;
ciphertext: string;
iv: string;
tag: string;

View File

@@ -1,8 +1,10 @@
import { Types } from 'mongoose';
import { handleEventHelper } from '../helpers/event';
interface Event {
name: string;
workspaceId: string;
workspaceId: Types.ObjectId;
environment?: string;
payload: any;
}

View File

@@ -52,9 +52,11 @@ class IntegrationService {
* @param {Object} obj.workspaceId - id of workspace
*/
static async syncIntegrations({
workspaceId
workspaceId,
environment
}: {
workspaceId: string;
workspaceId: Types.ObjectId;
environment?: string;
}) {
return await syncIntegrationsHelper({
workspaceId

View File

@@ -0,0 +1,183 @@
// WIP
import { Types } from 'mongoose';
import {
ISecret
} from '../models';
import {
CreateSecretParams,
GetSecretsParams,
GetSecretParams,
UpdateSecretParams,
DeleteSecretParams
} from '../interfaces/services/SecretService';
import {
initSecretBlindIndexDataHelper,
createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper,
generateSecretBlindIndexHelper,
createSecretHelper,
getSecretsHelper,
getSecretHelper,
updateSecretHelper,
deleteSecretHelper
} from '../helpers/secrets';
class SecretService {
/**
*
* @param param0 h
* @returns
*/
static async initSecretBlindIndexDataHelper() {
return await initSecretBlindIndexDataHelper();
}
/**
* Create secret blind index data containing encrypted blind index salt
* for workspace with id [workspaceId]
* @param {Object} obj
* @param {Buffer} obj.salt - 16-byte random salt
* @param {Types.ObjectId} obj.workspaceId
*/
static async createSecretBlindIndexData({
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) {
return await createSecretBlindIndexDataHelper({
workspaceId
});
}
/**
* Get secret blind index salt for workspace with id [workspaceId]
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
* @returns
*/
static async getSecretBlindIndexSalt({
workspaceId
}: {
workspaceId: Types.ObjectId;
}) {
return await getSecretBlindIndexSaltHelper({
workspaceId
});
}
/**
* Generate blind index for secret with name [secretName]
* and salt [salt]
* @param {Object} obj
* @param {Object} obj.secretName - name of secret to generate blind index for
* @param {String} obj.salt - base64-salt
*/
static async generateSecretBlindIndexWithSalt({
secretName,
salt
}: {
secretName: string;
salt: string;
}) {
return await generateSecretBlindIndexWithSaltHelper({
secretName,
salt
});
}
/**
* Create and return blind index for secret with
* name [secretName] part of workspace with id [workspaceId]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to generate blind index for
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
*/
static async generateSecretBlindIndex({
secretName,
workspaceId,
}: {
secretName: string;
workspaceId: Types.ObjectId;
}) {
return await generateSecretBlindIndexHelper({
secretName,
workspaceId
});
}
/**
* Create secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to create
* @param {Types.ObjectId} obj.workspaceId - id of workspace to create secret for
* @param {String} obj.environment - environment in workspace to create secret for
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async createSecret(createSecretParams: CreateSecretParams) {
return await createSecretHelper(createSecretParams);
}
/**
* Get secrets for workspace with id [workspaceId] and environment [environment]
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment in workspace
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async getSecrets(getSecretsParams: GetSecretsParams) {
return await getSecretsHelper(getSecretsParams);
}
/**
* Get secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to get
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async getSecret(getSecretParams: GetSecretParams) {
return await getSecretHelper(getSecretParams);
}
/**
* Update secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to update
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {String} obj.secretValueCiphertext - ciphertext of secret value
* @param {String} obj.secretValueIV - IV of secret value
* @param {String} obj.secretValueTag - tag of secret value
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async updateSecret(updateSecretParams: UpdateSecretParams) {
return await updateSecretHelper(updateSecretParams);
}
/**
* Delete secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to delete
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
static async deleteSecret(deleteSecretParams: DeleteSecretParams) {
return await deleteSecretHelper(deleteSecretParams);
}
}
export default SecretService;

View File

@@ -1,5 +1,6 @@
import { PostHog } from 'posthog-node';
import { getLogger } from '../utils/logger';
import { AuthData } from '../interfaces/middleware';
import {
getNodeEnv,
getTelemetryEnabled,
@@ -11,9 +12,11 @@ import {
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData
IServiceTokenData,
ServiceTokenData
} from '../models';
import {
AccountNotFoundError,
BadRequestError
} from '../utils/errors';
@@ -48,39 +51,30 @@ class Telemetry {
return postHogClient;
}
/**
* Return a distinct id for client to be used for logging telemetry
*/
static getDistinctId = ({
user,
serviceAccount,
serviceTokenData
static getDistinctId = async ({
authData
}: {
user?: IUser;
serviceAccount?: IServiceAccount;
serviceTokenData?: any; // TODO: fix (it's ServiceTokenData with user populated)
authData: AuthData;
}) => {
let distinctId = '';
if (user) {
distinctId = user.email;
if (authData.authPayload instanceof User) {
distinctId = authData.authPayload.email;
} else if (authData.authPayload instanceof ServiceAccount) {
distinctId = `sa.${authData.authPayload._id.toString()}`;
} else if (authData.authPayload instanceof ServiceTokenData) {
if (authData.authPayload.user) {
const user = await User.findById(authData.authPayload.user, 'email');
if (!user) throw AccountNotFoundError();
distinctId = user.email;
} else if (authData.authPayload.serviceAccount) {
distinctId = distinctId = `sa.${authData.authPayload.serviceAccount.toString()}`;
}
}
if (serviceAccount) {
distinctId = `sa.${serviceAccount._id.toString()}`;
}
if (serviceTokenData?.user && serviceTokenData?.user instanceof User) {
distinctId = serviceTokenData.user.email;
} else if (serviceTokenData?.serviceAccount && serviceTokenData?.serviceAccount instanceof ServiceAccount) {
distinctId = `sa.${serviceTokenData.serviceAccount._id.toString()}`;
}
if (distinctId === '') {
throw BadRequestError({
message: 'Failed to obtain distinct id for logging telemetry'
});
}
if (distinctId === '') throw BadRequestError({
message: 'Failed to obtain distinct id for logging telemetry'
});
return distinctId;
}

View File

@@ -5,14 +5,14 @@ import BotService from './BotService';
import EventService from './EventService';
import IntegrationService from './IntegrationService';
import TokenService from './TokenService';
import SecretService from './SecretService';
export {
TelemetryService,
// logTelemetryMessage,
// getPostHogClient,
DatabaseService,
BotService,
EventService,
IntegrationService,
TokenService
TokenService,
SecretService
}

View File

@@ -5,6 +5,9 @@ import {
IServiceTokenData,
ISecret
} from '../../models';
import {
AuthData
} from '../../interfaces/middleware';
// TODO: fix (any) types
declare global {
@@ -29,10 +32,7 @@ declare global {
serviceTokenData: any;
apiKeyData: any;
query?: any;
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
authData: AuthData;
requestData: {
[key: string]: string
};

View File

@@ -24,6 +24,7 @@ export interface BatchSecretRequest {
export interface BatchSecret {
_id: string;
type: 'shared' | 'personal',
secretBlindIndex: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;

54
backend/src/utils/auth.ts Normal file
View File

@@ -0,0 +1,54 @@
import { AuthData } from '../interfaces/middleware';
import {
User,
ServiceAccount,
ServiceTokenData,
ServiceToken
} from '../models';
// TODO: find a more optimal folder structure to store these types of functions
/**
* Returns an object containing the id of the authentication data payload
* @param {AuthData} authData - authentication data object
* @returns
*/
const getAuthDataPayloadIdObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { userId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceAccount) {
return { serviceAccountId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { serviceTokenDataId: authData.authPayload._id };
}
};
/**
* Returns an object containing the user associated with the authentication data payload
* @param {AuthData} authData - authentication data object
* @returns
*/
const getAuthDataPayloadUserObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { user: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceAccount) {
return { user: authData.authPayload.user };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { user: authData.authPayload.user };
}
}
export {
getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj
}

View File

@@ -153,6 +153,16 @@ export const SecretNotFoundError = (error?: Partial<RequestErrorContext>) => new
stack: error?.stack
});
//* ----->[SECRET BLIND INDEX DATA ERRORS]<-----
export const SecretBlindIndexDataNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 404,
type: error?.type ?? 'secret_blind_index_data_not_found_error',
message: error?.message ?? 'The requested secret was not found',
context: error?.context,
stack: error?.stack
});
//* ----->[SECRET SNAPSHOT ERRORS]<-----
export const SecretSnapshotNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,

View File

View File

@@ -220,7 +220,7 @@ const generateOpenAPISpec = async () => {
const outputJSONFile = '../spec.json';
const outputYAMLFile = '../docs/spec.yaml';
const endpointsFiles = ['../src/app.ts'];
const endpointsFiles = ['../src/index.ts'];
const spec = await swaggerAutogen(outputJSONFile, endpointsFiles, doc);
await fs.writeFile(outputYAMLFile, yaml.dump(spec.data));

View File

@@ -1,11 +1,11 @@
---
title: "Create"
openapi: "POST /api/v2/secrets/"
openapi: "POST /api/v3/secrets/{secretName}"
---
<Tip>
Using this route requires understanding Infisical's system and cryptography.
It may be helpful to read through the
[introduction](/api-reference/overview/introduction) and [guide for creating
secrets](/api-reference/overview/examples/create-secrets).
secrets](/api-reference/overview/examples/create-secret).
</Tip>

View File

@@ -1,4 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v2/secrets/"
openapi: "DELETE /api/v3/secrets/{secretName}"
---

View File

@@ -0,0 +1,11 @@
---
title: "Retrieve"
openapi: "GET /api/v3/secrets/{secretName}"
---
<Tip>
Using this route requires understanding Infisical's system and cryptography.
It may be helpful to read through the
[introduction](/api-reference/overview/introduction) and [guide for retrieving
secrets](/api-reference/overview/examples/retrieve-secret).
</Tip>

View File

@@ -1,11 +1,11 @@
---
title: "Retrieve"
openapi: "GET /api/v2/secrets/"
title: "Retrieve All"
openapi: "GET /api/v3/secrets/"
---
<Tip>
Using this route requires understanding Infisical's system and cryptography.
It may be helpful to read through the
[introduction](/api-reference/overview/introduction) and [guide for retrieving
secrets](/api-reference/overview/examples/retrieve-secrets).
secrets](/api-reference/overview/examples/retrieve-secret).
</Tip>

View File

@@ -1,11 +1,11 @@
---
title: "Update"
openapi: "PATCH /api/v2/secrets/"
openapi: "PATCH /api/v3/secrets/{secretName}"
---
<Tip>
Using this route requires understanding Infisical's system and cryptography.
It may be helpful to read through the
[introduction](/api-reference/overview/introduction) and [guide for updating
secrets](/api-reference/overview/examples/update-secrets).
secrets](/api-reference/overview/examples/update-secret).
</Tip>

View File

@@ -0,0 +1,18 @@
---
title: "Blind Indices"
---
In April 2023, we added the capability for users to query for secrets by name to improve the user experience of Infisical. Previously, it was only possible to query by id of the secret or fetch all secrets belonging to a project and environment.
Blind indexing must be enabled for projects created prior to April 2023 to take effect. If your project can be blind indexed, then you'll see a section in your project settings appear as shown below:
![project enable blind indices](../../images/project-settings-blind-indices.png)
It works using virtually irreversible blind indices generated by applying `argon2id` to the name of each secret and a random 128-bit salt assigned to each project on the server. We continue to keep the values of secrets E2EE by default.
You can read more about it [here](/security/mechanics).
<Note>
As previously mentioned, all projects made after April 2023 are automatically blind indexed. If you created a project before this date, you have to enable it manually in your project settings.
</Note>

View File

@@ -1,21 +1,21 @@
---
title: "Create secrets"
title: "Create secret"
description: "How to add a secret using an Infisical Token scoped 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.
- 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 with write access enabled.
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
- [Ensure that your project is blind-indexed](../blind-indices).
## Flow
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)
3. Encrypt your secret with the project key
4. [Send (encrypted) secret to Infisical](/api-reference/endpoints/secrets/create)
## Example
@@ -84,7 +84,7 @@ const createSecrets = async () => {
secret: serviceTokenSecret
});
// 3. Encrypt your secret(s) with the project key
// 3. Encrypt your secret with the project key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
@@ -111,27 +111,23 @@ const createSecrets = async () => {
text: secretComment,
secret: projectKey
});
const secret = {
type: secretType,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
}
// 4. Send (encrypted) secret(s) to Infisical
// 4. Send (encrypted) secret to Infisical
await axios.post(
`${BASE_URL}/api/v2/secrets`,
`${BASE_URL}/api/v3/secrets/${secretKey}`,
{
workspaceId: serviceTokenData.workspace,
environment: serviceTokenData.environment,
secrets: [secret]
type: secretType,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
},
{
headers: {
@@ -204,31 +200,27 @@ def create_secrets():
secret=service_token_secret,
)
# 3. Encrypt your secret(s) with the project key
# 3. Encrypt your secret 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
# 4. Send (encrypted) secret to Infisical
requests.post(
f"{BASE_URL}/api/v2/secrets",
f"{BASE_URL}/api/v3/secrets/{secret_key}",
json={
"workspaceId": service_token_data["workspace"],
"environment": service_token_data["environment"],
"secrets": [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"]
},
headers={"Authorization": f"Bearer {service_token}"},
)
@@ -238,10 +230,4 @@ 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>
</Tabs>

View File

@@ -0,0 +1,94 @@
---
title: "Delete secret"
description: "How to delete a secret using an Infisical Token scoped to a project and environment"
---
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 with write access enabled.
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
- [Ensure that your project is blind-indexed](../blind-indices).
## Example
<Tabs>
<Tab title="Javascript">
```js
const axios = require('axios');
const BASE_URL = 'https://app.infisical.com';
const deleteSecrets = async () => {
const serviceToken = 'your_service_token';
const secretType = 'shared' // 'shared' or 'personal'
const secretKey = 'some_key'
// 1. Get your Infisical Token data
const { data: serviceTokenData } = await axios.get(
`${BASE_URL}/api/v2/service-token`,
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
// 2. Delete secret from Infisical
await axios.delete(
`${BASE_URL}/api/v3/secrets/${secretKey}`,
{
workspaceId: serviceTokenData.workspace,
environment: serviceTokenData.environment,
type: secretType
},
{
headers: {
Authorization: `Bearer ${serviceToken}`
},
}
);
};
deleteSecrets();
```
</Tab>
<Tab title="Python">
```Python
import requests
BASE_URL = "https://app.infisical.com"
def delete_secrets():
service_token = "<your_service_token>"
secret_type = "shared" # "shared" or "personal"
secret_key = "some_key"
# 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. Delete secret from Infisical
requests.delete(
f"{BASE_URL}/api/v2/secrets/{secret_key}",
json={
"workspaceId": service_token_data["workspace"],
"environment": service_token_data["environment"],
"type": secret_type
},
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>

View File

@@ -1,70 +0,0 @@
---
title: "Delete secrets"
---
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 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(
`${BASE_URL}/api/v2/secrets`,
{
secretIds: [secretId],
},
{
headers: {
Authorization: `Bearer ${serviceToken}`
},
}
);
};
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>

View File

@@ -0,0 +1,180 @@
---
title: "Retrieve secret"
description: "How to get a secret using an Infisical Token scoped to a project and environment"
---
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).
- [Ensure that your project is blind-indexed](../blind-indices).
## Flow
1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key.
2. [Get the secret from your project and environment](/api-reference/endpoints/secrets/read-one).
3. Decrypt the (encrypted) project key with the key from your Infisical Token.
4. Decrypt the (encrypted) secret
## 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 decrypt = ({ ciphertext, iv, tag, secret}) => {
const decipher = crypto.createDecipheriv(
ALGORITHM,
secret,
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(tag, 'base64'));
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
cleartext += decipher.final('utf8');
return cleartext;
}
const getSecret = async () => {
const serviceToken = 'your_service_token';
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
const secretType = 'shared' // 'shared' or 'personal'
const secretKey = 'some_key';
// 1. Get your Infisical Token data
const { data: serviceTokenData } = await axios.get(
`${BASE_URL}/api/v2/service-token`,
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
// 2. Get the secret from your project and environment
const { data } = await axios.get(
`${BASE_URL}/api/v3/secrets/${secretKey}?${new URLSearchParams({
environment: serviceTokenData.environment,
workspaceId: serviceTokenData.workspace,
type: secretType // optional, defaults to 'shared'
})}`,
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
const encryptedSecret = data.secret;
// 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
});
// 4. Decrypt the (encrypted) secret value
const secretValue = decrypt({
ciphertext: encryptedSecret.secretValueCiphertext,
iv: encryptedSecret.secretValueIV,
tag: encryptedSecret.secretValueTag,
secret: projectKey
});
console.log('secret: ', ({
secretKey,
secretValue
}));
}
getSecret();
```
</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_secret():
service_token = "your_service_token"
service_token_secret = service_token[service_token.rindex(".") + 1 :]
secret_type = "shared" # "shared" or "personal"
secret_key = "some_key"
# 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 secret from your project and environment
data = requests.get(
f"{BASE_URL}/api/v3/secrets/{secret_key}",
params={
"environment": service_token_data["environment"],
"workspaceId": service_token_data["workspace"],
"type": secret_type # optional, defaults to "shared"
},
headers={"Authorization": f"Bearer {service_token}"},
).json()
encrypted_secret = data["secret"]
# 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) secret value
secret_value = decrypt(
ciphertext=encrypted_secret["secretValueCiphertext"],
iv=encrypted_secret["secretValueIV"],
tag=encrypted_secret["secretValueTag"],
secret=project_key,
)
print("secret: ", {
"secret_key": secret_key,
"secret_value": secret_value
})
get_secret()
```
</Tab>
</Tabs>

View File

@@ -1,14 +1,14 @@
---
title: "Retrieve secrets"
description: "How to get all secrets using an Infisical Token scoped to 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).
- 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).
- [Ensure that your project is blind-indexed](../blind-indices).
## Flow
@@ -58,7 +58,7 @@ const getSecrets = async () => {
// 2. Get secrets for your project and environment
const { data } = await axios.get(
`${BASE_URL}/api/v2/secrets?${new URLSearchParams({
`${BASE_URL}/api/v3/secrets?${new URLSearchParams({
environment: serviceTokenData.environment,
workspaceId: serviceTokenData.workspace
})}`,
@@ -143,7 +143,7 @@ def get_secrets():
# 2. Get secrets for your project and environment
data = requests.get(
f"{BASE_URL}/api/v2/secrets",
f"{BASE_URL}/api/v3/secrets",
params={
"environment": service_token_data["environment"],
"workspaceId": service_token_data["workspace"],
@@ -192,10 +192,4 @@ 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>
</Tabs>

View File

@@ -1,21 +1,21 @@
---
title: "Update secrets"
title: "Update secret"
description: "How to update a secret using an Infisical Token scoped to a project and environment"
---
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.
- 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 with write access enabled.
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
- [Ensure that your project is blind-indexed](../blind-indices).
## Flow
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)
3. Encrypt your updated secret with the project key
4. [Send (encrypted) updated secret to Infical](/api-reference/endpoints/secrets/update)
## Example
@@ -60,7 +60,7 @@ const updateSecrets = async () => {
const serviceToken = 'your_service_token';
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
const secretId = 'id_of_secret_to_update';
const secretType = 'shared' // 'shared' or 'personal'
const secretKey = 'some_key';
const secretValue = 'updated_value';
const secretComment = 'updated_comment';
@@ -83,7 +83,7 @@ const updateSecrets = async () => {
secret: serviceTokenSecret
});
// 3. Encrypt your updated secret(s) with the project key
// 3. Encrypt your updated secret with the project key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
@@ -110,27 +110,20 @@ const updateSecrets = async () => {
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
// 4. Send (encrypted) updated secret to Infisical
await axios.patch(
`${BASE_URL}/api/v2/secrets`,
`${BASE_URL}/api/v3/secrets/${secretKey}`,
{
secrets: [secret]
workspaceId: serviceTokenData.workspace,
environment: serviceTokenData.environment,
type: secretType,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
},
{
headers: {
@@ -184,7 +177,7 @@ 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_type = "shared" # "shared" or "personal"
secret_key = "some_key"
secret_value = "updated_value"
secret_comment = "updated_comment"
@@ -203,30 +196,28 @@ def update_secret():
secret=service_token_secret,
)
# 3. Encrypt your updated secret(s) with the project key
# 3. Encrypt your updated secret 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
# 4. Send (encrypted) updated secret to Infisical
requests.patch(
f"{BASE_URL}/api/v2/secrets",
json={"secrets": [secret]},
f"{BASE_URL}/api/v3/secrets/{secret_key}",
json={
"workspaceId": service_token_data["workspace"],
"environment": service_token_data["environment"],
"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"]
},
headers={"Authorization": f"Bearer {service_token}"},
)
@@ -235,10 +226,4 @@ 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>
</Tabs>

View File

@@ -14,9 +14,13 @@ With the Public API, users can create, read, update, and delete secrets, as well
If you decide to make your own requests using the API reference instead, be prepared for a steeper learning curve and more manual work.
</Warning>
<Warning>
In April 2023, we added the capability for users to query for secrets by name to improve the user experience of Infisical. If your project was created prior to April 2023, please read and follow the section on [blind indices](./blind-indices) and how to enable them for better usage of Infisical.
</Warning>
## Concepts
Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview).
Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview). A few key points:
- 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

View File

@@ -207,13 +207,15 @@
"pages": [
"api-reference/overview/introduction",
"api-reference/overview/authentication",
"api-reference/overview/blind-indices",
{
"group": "Examples",
"pages": [
"api-reference/overview/examples/create-secrets",
"api-reference/overview/examples/retrieve-secrets",
"api-reference/overview/examples/update-secrets",
"api-reference/overview/examples/delete-secrets"
"api-reference/overview/examples/create-secret",
"api-reference/overview/examples/retrieve-secret",
"api-reference/overview/examples/update-secret",
"api-reference/overview/examples/delete-secret"
]
}
]
@@ -252,8 +254,9 @@
{
"group": "Secrets",
"pages": [
"api-reference/endpoints/secrets/create",
"api-reference/endpoints/secrets/read",
"api-reference/endpoints/secrets/create",
"api-reference/endpoints/secrets/read-one",
"api-reference/endpoints/secrets/update",
"api-reference/endpoints/secrets/delete",
"api-reference/endpoints/secrets/versions",

View File

@@ -21,21 +21,30 @@ Infisical makes a usability-security tradeoff that is to give users convenient a
## Secrets
The `Secret` model includes the fields `workspace`, `type`, `user`, `environment`, `secretKeyCiphertext`, `secretKeyIV`, `secretKeyTag`, `secretValueCiphertext`, `secretValueIV`, and `secretValueTag`.
The `Secret` model includes the fields `workspace`, `type`, `user`, `environment`, `secretBlindIndex`, `secretKeyCiphertext`, `secretKeyIV`, `secretKeyTag`, `secretValueCiphertext`, `secretValueIV`, and `secretValueTag`.
Each secret is symmetrically encrypted by the key of the project that it belongs to; that key's encrypted copies are stored in a separate `Key` collection.
Each secret consists of a key name and value pair and is symmetrically encrypted by the key of the project that it belongs to; that key's encrypted copies are stored in a separate `Key` collection.
The `secretBlindIndex` enables users to query secrets by their names; it is a blind index computed by applying `argon2id` with a 128-bit random salt (unique to each project) and the name of the secret. The salt itself is symmetrically encrypted under the server key and stored in the `SecretBlindIndexData` collection.
## Blind Index Data
The `SecretBlindIndexData` model includes the fields `workspace`, `encryptedSaltCiphertext`, `saltIV`, and `saltTag`.
Infisical stores salts (unique to each project) symmetrically encrypted under the server key. The salts are used to compute blind indices for secrets that enable
users to query secrets by name.
## Project Keys
The `Key` model includes the fields `encryptedKey`, `nonce`, `sender`, `receiver`, and `workspace`.
Infisical stores copies of project keys, one for each member of a project, encrypted under each member's public key.
Infisical stores copies of project keys, one for each member of a project, asymmetrically encrypted under each member's public key.
## Bots
The `Bot` model contains the fields `name`, `workspace`, `isActive`, `publicKey`, `encryptedPrivateKey`, `iv`, and `tag`.
Each project comes with a bot that has its own public-private key pair; its private key is encrypted by the server's symmetric key. If needed, a user can opt-in to share their project key with the bot (i.e. Infisical) to give the platform access to the project's secrets.
Each project comes with a bot that has its own public-private key pair; its private key is symmetrically encrypted by the server's key. If needed, a user can opt-in to share their project key with the bot (i.e. Infisical) to give the platform access to the project's secrets.
<Note>
Sharing secrets with Infisical so they can be synced to integrations like

View File

@@ -27,3 +27,20 @@ After signing up, a user can invite other users to their organization to partake
To push secrets, a sender randomly-generates a symmetric encryption key, uses that key to encrypt their secret keys and values separately, asymmetrically encrypts the key with the receivers public keys, and uploads the encrypted secrets and keys to the server.
To pull secrets, a receiver obtains encrypted secret keys and values and their encrypted copy of the project key to decrypt the secrets from the server — they asymmetrically decrypt the key using their private key and use the decrypted key to decrypt the secrets. This public-key mechanism prevents the server-side from reading any secrets.
When dealing with individual secrets (e.g. pulling one secret by name) or creating new secrets, a user passes the name of the secret to the server which is then converted to a blind index by applying `argon2id` with the name of the secret and a 128-bit random salt unique to each project; the salt itself is encrypted by the server key and stored in the database.
<Info>
Infisical ensures that the name of any secret is never stored in plaintext and instead only a blind index generated from the name. It is infeasible to reverse back a blind index to the name of a secret without knowledge of the server key.
</Info>
## Bot
To use some features like integrations, users must opt out of E2EE (this means sharing access to secrets with Infisical).
Infisical employs the concept of a bot which is a cryptographic abstraction for how Infisical interacts with secrets when users opt out of E2EE. In this model, each project is assigned a bot with its own public-private key pair where each bot's private key is stored symmetrically encrypted under the server key. When a user opts out of E2EE, they share the project key with the bot by encrypting a copy of it under the public key of the bot.
When users wish to sync secrets from a project and environment Infisical to other platform integrations like Vercel or GitHub, Infisical decrypts the intended secrets and uses the integration platform's APIs to send secrets over. It should be noted that opting out of E2EE is optional and it is entirely possible to use Infisical to manage secrets across your team and infrastructure without opting out of E2EE.

View File

@@ -5,7 +5,7 @@ description: "Infisical's security statement."
## Summary
Infisical uses end-to-end encryption (E2EE) whenever possible to securely store and share secrets. It uses secure remote password (SRP) to handle authentication and public-key cryptography for secret sharing and syncing; secrets are symmetrically encrypted at rest by keys decryptable only by members of the project.
Infisical uses end-to-end encryption (E2EE) whenever possible to securely store and share secret values. It uses secure remote password (SRP) to handle authentication and public-key cryptography for secret sharing and syncing; secrets are symmetrically encrypted by keys decryptable only by members of the project.
Infisical uses AES256-GCM for symmetric encryption and x25519-xsalsa20-poly1305 for asymmetric encryption operations mentioned in this brief; key generation and asymmetric algorithms are implemented with the [TweetNaCl.js](https://tweetnacl.js.org/#/) library which has been well-audited and recommended for use by cybersecurity firm Cure53. Lastly, the secure remote password (SRP) implementation uses [jsrp](https://github.com/alax/jsrp) package for user authentication. As part of our commitment to user privacy and security, we aim to conduct formal security and compliance audits in the following year.

View File

@@ -940,7 +940,11 @@ paths:
/api/v1/invite-org/signup:
post:
description: ''
parameters: []
parameters:
- name: host
in: header
schema:
type: string
responses:
'200':
description: OK
@@ -1277,6 +1281,12 @@ paths:
example: any
targetEnvironment:
example: any
targetEnvironmentId:
example: any
targetService:
example: any
targetServiceId:
example: any
owner:
example: any
path:
@@ -1415,11 +1425,75 @@ paths:
required: true
schema:
type: string
- name: teamId
in: query
schema:
type: string
responses:
'200':
description: OK
'400':
description: Bad Request
/api/v1/integration-auth/{integrationAuthId}/teams:
get:
description: ''
parameters:
- name: integrationAuthId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
/api/v1/integration-auth/{integrationAuthId}/vercel/branches:
get:
description: ''
parameters:
- name: integrationAuthId
in: path
required: true
schema:
type: string
- name: appId
in: query
schema:
type: string
responses:
'200':
description: OK
/api/v1/integration-auth/{integrationAuthId}/railway/environments:
get:
description: ''
parameters:
- name: integrationAuthId
in: path
required: true
schema:
type: string
- name: appId
in: query
schema:
type: string
responses:
'200':
description: OK
/api/v1/integration-auth/{integrationAuthId}/railway/services:
get:
description: ''
parameters:
- name: integrationAuthId
in: path
required: true
schema:
type: string
- name: appId
in: query
schema:
type: string
responses:
'200':
description: OK
/api/v2/signup/complete-account/signup:
post:
description: ''
@@ -1771,10 +1845,20 @@ paths:
items:
$ref: '#/components/schemas/Project'
description: Projects of organization
'400':
description: Bad Request
security:
- apiKeyAuth: []
/api/v2/organizations/{organizationId}/service-accounts:
get:
description: ''
parameters:
- name: organizationId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
/api/v2/workspace/{workspaceId}/environments:
post:
description: ''
@@ -2467,8 +2551,6 @@ paths:
responses:
'200':
description: OK
'400':
description: Bad Request
requestBody:
content:
application/json:
@@ -2503,8 +2585,139 @@ paths:
responses:
'200':
description: OK
/api/v2/service-accounts/me:
get:
description: ''
parameters: []
responses:
'200':
description: OK
/api/v2/service-accounts/{serviceAccountId}:
get:
description: ''
parameters:
- name: serviceAccountId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
delete:
description: ''
parameters:
- name: serviceAccountId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
/api/v2/service-accounts/:
post:
description: ''
parameters: []
responses:
'200':
description: OK
/api/v2/service-accounts/{serviceAccountId}/name:
patch:
description: ''
parameters:
- name: serviceAccountId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
example: any
/api/v2/service-accounts/{serviceAccountId}/permissions/workspace:
get:
description: ''
parameters:
- name: serviceAccountId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
post:
description: ''
parameters:
- name: serviceAccountId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
'400':
description: Bad Request
requestBody:
content:
application/json:
schema:
type: object
properties:
environment:
example: any
workspaceId:
example: any
read:
example: any
write:
example: any
encryptedKey:
example: any
nonce:
example: any
/api/v2/service-accounts/{serviceAccountId}/permissions/workspace/{serviceAccountWorkspacePermissionId}:
delete:
description: ''
parameters:
- name: serviceAccountId
in: path
required: true
schema:
type: string
- name: serviceAccountWorkspacePermissionId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
/api/v2/service-accounts/{serviceAccountId}/keys:
get:
description: ''
parameters:
- name: serviceAccountId
in: path
required: true
schema:
type: string
- name: workspaceId
in: query
schema:
type: string
responses:
'200':
description: OK
/api/v2/api-key/:
get:
description: ''
@@ -2546,6 +2759,182 @@ paths:
description: OK
'400':
description: Bad Request
/api/v3/secrets/:
get:
description: ''
parameters:
- name: workspaceId
in: query
schema:
type: string
- name: environment
in: query
schema:
type: string
responses:
'200':
description: OK
/api/v3/secrets/{secretName}:
post:
description: ''
parameters:
- name: secretName
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
requestBody:
content:
application/json:
schema:
type: object
properties:
workspaceId:
example: any
environment:
example: any
type:
example: any
secretKeyCiphertext:
example: any
secretKeyIV:
example: any
secretKeyTag:
example: any
secretValueCiphertext:
example: any
secretValueIV:
example: any
secretValueTag:
example: any
secretCommentCiphertext:
example: any
secretCommentIV:
example: any
secretCommentTag:
example: any
get:
description: ''
parameters:
- name: secretName
in: path
required: true
schema:
type: string
- name: workspaceId
in: query
schema:
type: string
- name: environment
in: query
schema:
type: string
- name: type
in: query
schema:
type: string
responses:
'200':
description: OK
patch:
description: ''
parameters:
- name: secretName
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
requestBody:
content:
application/json:
schema:
type: object
properties:
workspaceId:
example: any
environment:
example: any
type:
example: any
secretValueCiphertext:
example: any
secretValueIV:
example: any
secretValueTag:
example: any
delete:
description: ''
parameters:
- name: secretName
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
requestBody:
content:
application/json:
schema:
type: object
properties:
workspaceId:
example: any
environment:
example: any
type:
example: any
/api/v3/workspaces/{workspaceId}/secrets/blind-index-status:
get:
description: ''
parameters:
- name: workspaceId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
/api/v3/workspaces/{workspaceId}/secrets:
get:
description: ''
parameters:
- name: workspaceId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
/api/v3/workspaces/{workspaceId}/secrets/names:
post:
description: ''
parameters:
- name: workspaceId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
requestBody:
content:
application/json:
schema:
type: object
properties:
secretsToUpdate:
example: any
/api/status:
get:
description: ''

View File

@@ -10,6 +10,7 @@ interface EncryptedSecretProps {
id: string;
createdAt: string;
environment: string;
secretName: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
@@ -94,6 +95,7 @@ const encryptSecrets = async ({
id: secret.id,
createdAt: '',
environment: env,
secretName: secret.key,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,

View File

@@ -93,6 +93,7 @@ const initProjectHelper = async ({
}) => {
let project;
try {
// create new project
project = await createWorkspace({
workspaceName: projectName,

View File

@@ -7,7 +7,9 @@ export {
useGetUserWorkspaces,
useGetUserWsEnvironments,
useGetWorkspaceById,
useGetWorkspaceIndexStatus,
useGetWorkspaceSecrets,
useNameWorkspaceSecrets,
useRenameWorkspace,
useToggleAutoCapitalization,
useUpdateWsEnvironment
} from './queries';
useUpdateWsEnvironment} from './queries';

View File

@@ -2,12 +2,16 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import {
EncryptedSecret
} from '../secrets/types';
import {
CreateEnvironmentDTO,
CreateWorkspaceDTO,
DeleteEnvironmentDTO,
DeleteWorkspaceDTO,
GetWsEnvironmentDTO,
NameWorkspaceSecretsDTO,
RenameWorkspaceDTO,
ToggleAutoCapitalizationDTO,
UpdateEnvironmentDTO,
@@ -17,6 +21,8 @@ import {
const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, 'workspace-secrets'] as const,
getWorkspaceIndexStatus: (workspaceId: string) => [{ workspaceId}, 'workspace-index-status'] as const,
getWorkspaceMemberships: (orgId: string) => [{ orgId }, 'workspace-memberships'],
getAllUserWorkspace: ['workspaces'] as const,
getUserWsEnvironments: (workspaceId: string) => ['workspace-env', { workspaceId }] as const
@@ -26,14 +32,47 @@ const fetchWorkspaceById = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ workspace: Workspace }>(
`/api/v1/workspace/${workspaceId}`
);
return data.workspace;
};
const fetchWorkspaceIndexStatus = async (workspaceId: string) => {
const { data } = await apiRequest.get<boolean>(
`/api/v3/workspaces/${workspaceId}/secrets/blind-index-status`
);
return data;
}
const fetchWorkspaceSecrets = async (workspaceId: string) => {
const { data: { secrets } } = await apiRequest.get<{ secrets: EncryptedSecret[] }>(
`/api/v3/workspaces/${workspaceId}/secrets`
);
return secrets;
}
const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace');
return data.workspaces;
};
export const useGetWorkspaceIndexStatus = (workspaceId: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceIndexStatus(workspaceId),
queryFn: () => fetchWorkspaceIndexStatus(workspaceId),
enabled: true
});
}
export const useGetWorkspaceSecrets = (workspaceId: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceSecrets(workspaceId),
queryFn: () => fetchWorkspaceSecrets(workspaceId),
enabled: true
})
}
export const useGetWorkspaceById = (workspaceId: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceById(workspaceId),
@@ -75,6 +114,20 @@ export const useGetUserWorkspaceMemberships = (orgId: string) =>
enabled: Boolean(orgId)
});
export const useNameWorkspaceSecrets = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, NameWorkspaceSecretsDTO>({
mutationFn: async ({ workspaceId, secretsToUpdate }) =>
apiRequest.post(`/api/v3/workspaces/${workspaceId}/secrets/names`, {
secretsToUpdate
}),
onSuccess: (_, variables) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIndexStatus(variables.workspaceId));
}
});
}
// mutation
export const useCreateWorkspace = () => {
const queryClient = useQueryClient();

View File

@@ -16,6 +16,14 @@ export type WorkspaceEnv = {
export type WorkspaceTag = { _id: string; name: string; slug: string };
export type NameWorkspaceSecretsDTO = {
workspaceId: string;
secretsToUpdate: {
secretName: string;
_id: string;
}[];
}
// mutation dto
export type CreateWorkspaceDTO = {
workspaceName: string;

View File

@@ -586,6 +586,7 @@ export default function Dashboard() {
method: 'POST',
secret: {
type: secret.type,
secretName: secret.secretName,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
@@ -614,6 +615,7 @@ export default function Dashboard() {
secret: {
_id: secret.id,
type: secret.type,
secretName: secret.secretName,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,

View File

@@ -9,8 +9,8 @@ import NavHeader from '@app/components/navigation/NavHeader';
// TODO(akhilmhdh):Refactor this into a better utility module package
import {
decryptAssymmetric,
encryptSymmetric
} from '@app/components/utilities/cryptography/crypto';
decryptSymmetric,
encryptSymmetric} from '@app/components/utilities/cryptography/crypto';
import { Button, FormControl, Input } from '@app/components/v2';
import { plans } from '@app/const';
import { useSubscription, useWorkspace } from '@app/context';
@@ -25,11 +25,13 @@ import {
useDeleteWsTag,
useGetUserWsKey,
useGetUserWsServiceTokens,
useGetWorkspaceIndexStatus,
useGetWorkspaceSecrets,
useGetWsTags,
useNameWorkspaceSecrets,
useRenameWorkspace,
useToggleAutoCapitalization,
useUpdateWsEnvironment
} from '@app/hooks/api';
useUpdateWsEnvironment} from '@app/hooks/api';
import { AutoCapitalizationSection } from './components/AutoCapitalizationSection/AutoCapitalizationSection';
import { SecretTagsSection } from './components/SecretTagsSection';
@@ -39,6 +41,7 @@ import {
CreateUpdateEnvFormData,
CreateWsTag,
EnvironmentSection,
ProjectIndexSecretsSection,
ProjectNameChangeSection,
ServiceTokenSection
} from './components';
@@ -55,6 +58,7 @@ export const ProjectSettingsPage = () => {
const [isDeleting, setIsDeleting] = useToggle();
const renameWorkspace = useRenameWorkspace();
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
const toggleAutoCapitalization = useToggleAutoCapitalization();
const deleteWorkspace = useDeleteWorkspace();
@@ -63,11 +67,16 @@ export const ProjectSettingsPage = () => {
const updateWsEnv = useUpdateWsEnvironment();
const deleteWsEnv = useDeleteWsEnvironment();
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(workspaceID);
// service token
const { data: serviceTokens, isLoading: isServiceTokenLoading } = useGetUserWsServiceTokens({
workspaceID: currentWorkspace?._id || ''
});
const { data: latestFileKey } = useGetUserWsKey(workspaceID);
const { data: encryptedSecrets } = useGetWorkspaceSecrets(workspaceID);
const createServiceToken = useCreateServiceToken();
const deleteServiceToken = useDeleteServiceToken();
@@ -207,14 +216,15 @@ export const ProjectSettingsPage = () => {
// type guard
if (!latestFileKey) return '';
try {
// crypo calculation to generate the key
const key = decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: localStorage.getItem('PRIVATE_KEY') as string
});
const randomBytes = crypto.randomBytes(16).toString('hex');
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: key,
key: randomBytes
@@ -303,6 +313,38 @@ export const ProjectSettingsPage = () => {
}
};
const onEnableBlindIndices = async () => {
if (!currentWorkspace?._id) return;
if (!encryptedSecrets) return;
if (!latestFileKey) return;
const key = decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: localStorage.getItem('PRIVATE_KEY') as string
});
const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => {
const secretName = decryptSymmetric({
ciphertext: encryptedSecret.secretKeyCiphertext,
iv: encryptedSecret.secretKeyIV,
tag: encryptedSecret.secretKeyTag,
key
});
return ({
secretName,
_id: encryptedSecret._id
});
});
await nameWorkspaceSecrets.mutateAsync({
workspaceId: currentWorkspace._id,
secretsToUpdate
});
}
return (
<div className="dark container mx-auto flex flex-col px-8 text-mineshaft-50 dark:[color-scheme:dark]">
{/* TODO(akhilmhdh): Remove this right when layout is refactored */}
@@ -349,6 +391,11 @@ export const ProjectSettingsPage = () => {
workspaceAutoCapitalization={currentWorkspace?.autoCapitalization}
onAutoCapitalizationChange={onAutoCapitalizationToggle}
/>
{!isBlindIndexedLoading && !isBlindIndexed && (
<ProjectIndexSecretsSection
onEnableBlindIndices={onEnableBlindIndices}
/>
)}
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md border-l border-red bg-white/5 px-6 pl-6 pb-4 pt-4">
<p className="text-xl font-bold text-red">{t('settings-project:danger-zone')}</p>
<p className="text-md mt-2 text-gray-400">{t('settings-project:danger-zone-note')}</p>

View File

@@ -0,0 +1,32 @@
import { Button } from '@app/components/v2';
// TODO: add check so that this only shows up if user is
// an admin in the workspace
type Props = {
onEnableBlindIndices: () => Promise<void>;
}
export const ProjectIndexSecretsSection = ({
onEnableBlindIndices
}: Props) => {
return (
<div className="rounded-md bg-white/5 p-6">
<p className="mb-4 text-xl font-semibold">Blind Indices</p>
<p className="mb-4 text-sm text-gray-400">
Your project, created before the introduction of blind indexing, contains unindexed secrets. To access individual secrets by name through the SDK and public API, please enable blind indexing.
</p>
<p className="mb-4 text-sm text-gray-400">
Learn more about it here.
</p>
<Button
onClick={onEnableBlindIndices}
color="mineshaft"
size="sm"
type="submit"
>
Enable Blind Indexing
</Button>
</div>
);
}

View File

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

View File

@@ -1,6 +1,7 @@
export { CopyProjectIDSection } from './CopyProjectIDSection';
export { EnvironmentSection } from './EnvironmentSection';
export type { CreateUpdateEnvFormData } from './EnvironmentSection/EnvironmentSection';
export { ProjectIndexSecretsSection } from './ProjectIndexSecretsSection';
export { ProjectNameChangeSection } from './ProjectNameChangeSection';
export type { CreateWsTag } from './SecretTagsSection/SecretTagsSection';
export { ServiceTokenSection } from './ServiceTokenSection';