diff --git a/README.md b/README.md index 756f65f92..3a3860d2c 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,9 @@ - **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.) - **Personal overrides** for secrets and configs - **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure -- **[Secret Versioning](https://infisical.com/docs/getting-started/dashboard/versioning)** - check the history of change for any secret -- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** - check what user in the project is performing what actions with secrets -- **[Point-in-time Secrets Recovery](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** - roll back to any snapshot of you secrets +- **[Secret Versioning](https://infisical.com/docs/getting-started/dashboard/versioning)** to view the change history for any secret +- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** to record every action taken in a project. +- **[Point-in-time Secrets Recovery](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** for rolling back to any snapshot of your secrets - 🔜 **1-Click Deploy** to Digital Ocean and Heroku - 🔜 **Authentication/Authorization** for projects (read/write controls soon) - 🔜 **Automatic Secret Rotation** @@ -338,6 +338,10 @@ Infisical officially launched as v.1.0 on November 21st, 2022. There are a lot o ## 🌎 Translations +<<<<<<< HEAD +Infisical is currently aviable in English and Korean. Help us translate Infisical to your language! +======= Infisical is currently available in English and Korean. Help us translate Infisical to your language! +>>>>>>> 9ce4a52b8da0057c2450cd7af93a8c5758c2476b -You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181). \ No newline at end of file +You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181). diff --git a/backend/package-lock.json b/backend/package-lock.json index b51aa7cf0..38c1126cc 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -28,6 +28,7 @@ "express-validator": "^6.14.2", "handlebars": "^4.7.7", "helmet": "^5.1.1", + "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", "libsodium-wrappers": "^0.7.10", @@ -3698,8 +3699,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-flatten": { "version": "1.1.1", @@ -6638,7 +6638,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -14980,8 +14979,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "array-flatten": { "version": "1.1.1", @@ -17197,7 +17195,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "requires": { "argparse": "^2.0.1" } diff --git a/backend/package.json b/backend/package.json index 08f5a815c..b9359f680 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,7 @@ "scripts": { "start": "npm run build && node build/index.js", "dev": "nodemon", - "swagger-autogen": "node ./swagger.ts", + "swagger-autogen": "node ./swagger/index.ts", "build": "rimraf ./build && tsc && cp -R ./src/templates ./build", "lint": "eslint . --ext .ts", "lint-and-fix": "eslint . --ext .ts --fix", @@ -94,6 +94,7 @@ "express-validator": "^6.14.2", "handlebars": "^4.7.7", "helmet": "^5.1.1", + "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", "libsodium-wrappers": "^0.7.10", diff --git a/backend/api-documentation.json b/backend/spec.json similarity index 65% rename from backend/api-documentation.json rename to backend/spec.json index 79cf0eb3b..e28c66eaa 100644 --- a/backend/api-documentation.json +++ b/backend/spec.json @@ -5,10 +5,21 @@ "description": "List of all available APIs that can be consumed", "version": "1.0.0" }, + "servers": [ + { + "url": "https://infisical.com", + "description": "Production server" + }, + { + "url": "http://localhost:8080", + "description": "Local server" + } + ], "paths": { "/api/v1/secret/{secretId}/secret-versions": { "get": { - "description": "", + "summary": "Return secret versions", + "description": "Return secret versions", "parameters": [ { "name": "secretId", @@ -16,10 +27,13 @@ "required": true, "schema": { "type": "string" - } + }, + "description": "ID of secret" }, { "name": "offset", + "description": "Number of versions to skip", + "required": false, "in": "query", "schema": { "type": "string" @@ -27,6 +41,8 @@ }, { "name": "limit", + "description": "Maximum number of versions to return", + "required": false, "in": "query", "schema": { "type": "string" @@ -35,11 +51,92 @@ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secretVersions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SecretVersion" + }, + "description": "Secret versions" + } + } + } + } + } }, "400": { "description": "Bad Request" } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + } + }, + "/api/v1/secret/{secretId}/secret-versions/rollback": { + "post": { + "summary": "Roll back secret to a version.", + "description": "Roll back secret to a version.", + "parameters": [ + { + "name": "secretId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID of secret" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secret": { + "type": "object", + "$ref": "#/components/schemas/Secret", + "description": "Secret rolled back to" + } + } + } + } + } + }, + "400": { + "description": "Bad Request" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "version": { + "type": "integer", + "description": "Version of secret to roll back to" + } + } + } + } + } } } }, @@ -68,7 +165,8 @@ }, "/api/v1/workspace/{workspaceId}/secret-snapshots": { "get": { - "description": "", + "summary": "Return project secret snapshot ids", + "description": "Return project secret snapshots ids", "parameters": [ { "name": "workspaceId", @@ -76,10 +174,13 @@ "required": true, "schema": { "type": "string" - } + }, + "description": "ID of project" }, { "name": "offset", + "description": "Number of secret snapshots to skip", + "required": false, "in": "query", "schema": { "type": "string" @@ -87,6 +188,8 @@ }, { "name": "limit", + "description": "Maximum number of secret snapshots to return", + "required": false, "in": "query", "schema": { "type": "string" @@ -95,12 +198,33 @@ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secretSnapshots": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SecretSnapshot" + }, + "description": "Project secret snapshots" + } + } + } + } + } }, "400": { "description": "Bad Request" } - } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] } }, "/api/v1/workspace/{workspaceId}/secret-snapshots/count": { @@ -126,20 +250,95 @@ } } }, - "/api/v1/workspace/{workspaceId}/logs": { - "get": { - "description": "", + "/api/v1/workspace/{workspaceId}/secret-snapshots/rollback": { + "post": { + "summary": "Roll back project secrets to those captured in a secret snapshot version.", + "description": "Roll back project secrets to those captured in a secret snapshot version.", "parameters": [ { "name": "workspaceId", "in": "path", "required": true, + "schema": { + "type": "string" + }, + "description": "ID of project" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secrets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Secret" + }, + "description": "Secrets rolled back to" + } + } + } + } + } + }, + "400": { + "description": "Bad Request" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "version": { + "type": "integer", + "description": "Version of secret snapshot to roll back to" + } + } + } + } + } + } + } + }, + "/api/v1/workspace/{workspaceId}/logs": { + "get": { + "summary": "Return project (audit) logs", + "description": "Return project (audit) logs", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID of project" + }, + { + "name": "userId", + "description": "ID of project member", + "required": false, + "in": "query", "schema": { "type": "string" } }, { "name": "offset", + "description": "Number of logs to skip", + "required": false, "in": "query", "schema": { "type": "string" @@ -147,6 +346,8 @@ }, { "name": "limit", + "description": "Maximum number of logs to return", + "required": false, "in": "query", "schema": { "type": "string" @@ -154,20 +355,21 @@ }, { "name": "sortBy", - "in": "query", + "description": "Order to sort the logs by", "schema": { - "type": "string" - } - }, - { - "name": "userId", - "in": "query", - "schema": { - "type": "string" - } + "type": "string", + "enum": [ + "oldest", + "recent" + ] + }, + "required": false, + "in": "query" }, { "name": "actionNames", + "description": "Names of log actions (comma-separated)", + "required": false, "in": "query", "schema": { "type": "string" @@ -176,12 +378,33 @@ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "logs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Log" + }, + "description": "Project logs" + } + } + } + } + } }, "400": { "description": "Bad Request" } - } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] } }, "/api/v1/action/{actionId}": { @@ -476,7 +699,11 @@ "post": { "description": "", "parameters": [], - "responses": {} + "responses": { + "200": { + "description": "OK" + } + } } }, "/api/v1/bot/{workspaceId}": { @@ -1997,6 +2224,40 @@ } } }, + "/api/v2/users/me": { + "get": { + "summary": "Retrieve the current user on the request", + "description": "Retrieve the current user on the request", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "user": { + "type": "object", + "$ref": "#/components/schemas/CurrentUser", + "description": "Current user on request" + } + } + } + } + } + }, + "400": { + "description": "Bad Request" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + } + }, "/api/v2/workspace/{workspaceId}/secrets": { "post": { "description": "", @@ -2080,7 +2341,8 @@ }, "/api/v2/workspace/{workspaceId}/encrypted-key": { "get": { - "description": "", + "summary": "Return encrypted project key", + "description": "Return encrypted project key", "parameters": [ { "name": "workspaceId", @@ -2088,17 +2350,34 @@ "required": true, "schema": { "type": "string" - } + }, + "description": "ID of project" } ], "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectKey" + }, + "description": "Encrypted project key for the given project" + } + } + } }, "400": { "description": "Bad Request" } - } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] } }, "/api/v2/workspace/{workspaceId}/service-token-data": { @@ -2124,7 +2403,171 @@ } } }, - "/api/v2/secret/batch-create/workspace/{workspaceId}/environment/{environmentName}": { + "/api/v2/workspace/{workspaceId}/memberships": { + "get": { + "summary": "Return project memberships", + "description": "Return project memberships", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID of project" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "memberships": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Membership" + }, + "description": "Memberships of project" + } + } + } + } + } + }, + "400": { + "description": "Bad Request" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + } + }, + "/api/v2/workspace/{workspaceId}/memberships/{membershipId}": { + "delete": { + "summary": "Delete project membership", + "description": "Delete project membership", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID of project" + }, + { + "name": "membershipId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID of membership" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "membership": { + "$ref": "#/components/schemas/Membership", + "description": "Deleted membership" + } + } + } + } + } + }, + "400": { + "description": "Bad Request" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + }, + "patch": { + "summary": "Update project membership", + "description": "Update project membership", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID of project" + }, + { + "name": "membershipId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID of membership" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "membership": { + "$ref": "#/components/schemas/Membership", + "description": "Updated membership" + } + } + } + } + } + }, + "400": { + "description": "Bad Request" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": "Role of membership - either admin or member" + } + } + } + } + } + } + } + }, + "/api/v2/secret/batch-create/workspace/{workspaceId}/environment/{environment}": { "post": { "description": "", "parameters": [ @@ -2137,7 +2580,7 @@ } }, { - "name": "environmentName", + "name": "environment", "in": "path", "required": true, "schema": { @@ -2166,7 +2609,7 @@ } } }, - "/api/v2/secret/workspace/{workspaceId}/environment/{environmentName}": { + "/api/v2/secret/workspace/{workspaceId}/environment/{environment}": { "post": { "description": "", "parameters": [ @@ -2179,47 +2622,7 @@ } }, { - "name": "environmentName", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "secret": { - "example": "any" - } - } - } - } - } - } - }, - "patch": { - "description": "", - "parameters": [ - { - "name": "workspaceId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "environmentName", + "name": "environment", "in": "path", "required": true, "schema": { @@ -2397,6 +2800,256 @@ } } }, + "/api/v2/secret/workspace/{workspaceId}/environment/{environmentName}": { + "patch": { + "description": "", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "environmentName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secret": { + "example": "any" + } + } + } + } + } + } + } + }, + "/api/v2/secrets/": { + "post": { + "summary": "Create new secret(s)", + "description": "Create one or many secrets for a given project and environment.", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secrets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Secret" + }, + "description": "Newly-created secrets for the given project and environment" + } + } + } + } + } + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "description": "ID of project" + }, + "environment": { + "type": "string", + "description": "Environment within project" + }, + "secrets": { + "$ref": "#/components/schemas/CreateSecret", + "description": "Secret(s) to create - object or array of objects" + } + } + } + } + } + } + }, + "get": { + "summary": "Read secrets", + "description": "Read secrets from a project and environment", + "parameters": [ + { + "name": "workspaceId", + "description": "ID of project", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "environment", + "description": "Environment within project", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "content", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secrets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Secret" + }, + "description": "Secrets for the given project and environment" + } + } + } + } + } + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + }, + "patch": { + "summary": "Update secret(s)", + "description": "Update secret(s)", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secrets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Secret" + }, + "description": "Updated secrets" + } + } + } + } + } + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secrets": { + "$ref": "#/components/schemas/UpdateSecret", + "description": "Secret(s) to update - object or array of objects" + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete secret(s)", + "description": "Delete one or many secrets by their ID(s)", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secrets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Secret" + }, + "description": "Deleted secrets" + } + } + } + } + } + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secretIds": { + "type": "string", + "description": "ID(s) of secrets - string or array of strings" + } + } + } + } + } + } + } + }, "/api/v2/service-token/": { "get": { "description": "", @@ -2557,11 +3210,478 @@ } }, "components": { + "schemas": { + "CurrentUser": { + "type": "object", + "properties": { + "_id": { + "type": "string", + "example": "" + }, + "email": { + "type": "string", + "example": "" + }, + "firstName": { + "type": "string", + "example": "" + }, + "lastName": { + "type": "string", + "example": "" + }, + "publicKey": { + "type": "string", + "example": "" + }, + "encryptedPrivateKey": { + "type": "string", + "example": "" + }, + "iv": { + "type": "string", + "example": "" + }, + "tag": { + "type": "string", + "example": "" + }, + "updatedAt": { + "type": "string", + "example": "" + }, + "createdAt": { + "type": "string", + "example": "" + } + } + }, + "Membership": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "_id": { + "type": "string", + "example": "" + }, + "email": { + "type": "string", + "example": "" + }, + "firstName": { + "type": "string", + "example": "" + }, + "lastName": { + "type": "string", + "example": "" + }, + "publicKey": { + "type": "string", + "example": "" + }, + "updatedAt": { + "type": "string", + "example": "" + }, + "createdAt": { + "type": "string", + "example": "" + } + } + }, + "workspace": { + "type": "string", + "example": "" + }, + "role": { + "type": "string", + "example": "admin" + } + } + }, + "ProjectKey": { + "type": "object", + "properties": { + "encryptedkey": { + "type": "string", + "example": "" + }, + "nonce": { + "type": "string", + "example": "" + }, + "sender": { + "type": "object", + "properties": { + "publicKey": { + "type": "string", + "example": "" + } + } + }, + "receiver": { + "type": "string", + "example": "" + }, + "workspace": { + "type": "string", + "example": "" + } + } + }, + "CreateSecret": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "shared" + }, + "secretKeyCiphertext": { + "type": "string", + "example": "" + }, + "secretKeyIV": { + "type": "string", + "example": "" + }, + "secretKeyTag": { + "type": "string", + "example": "" + }, + "secretValueCiphertext": { + "type": "string", + "example": "" + }, + "secretValueIV": { + "type": "string", + "example": "" + }, + "secretValueTag": { + "type": "string", + "example": "" + }, + "secretCommentCiphertext": { + "type": "string", + "example": "" + }, + "secretCommentIV": { + "type": "string", + "example": "" + }, + "secretCommentTag": { + "type": "string", + "example": "" + } + } + }, + "UpdateSecret": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "" + }, + "secretKeyCiphertext": { + "type": "string", + "example": "" + }, + "secretKeyIV": { + "type": "string", + "example": "" + }, + "secretKeyTag": { + "type": "string", + "example": "" + }, + "secretValueCiphertext": { + "type": "string", + "example": "" + }, + "secretValueIV": { + "type": "string", + "example": "" + }, + "secretValueTag": { + "type": "string", + "example": "" + }, + "secretCommentCiphertext": { + "type": "string", + "example": "" + }, + "secretCommentIV": { + "type": "string", + "example": "" + }, + "secretCommentTag": { + "type": "string", + "example": "" + } + } + }, + "Secret": { + "type": "object", + "properties": { + "_id": { + "type": "string", + "example": "" + }, + "version": { + "type": "number", + "example": 1 + }, + "workspace": { + "type": "string", + "example": "" + }, + "type": { + "type": "string", + "example": "shared" + }, + "user": {}, + "secretKeyCiphertext": { + "type": "string", + "example": "" + }, + "secretKeyIV": { + "type": "string", + "example": "" + }, + "secretKeyTag": { + "type": "string", + "example": "" + }, + "secretValueCiphertext": { + "type": "string", + "example": "" + }, + "secretValueIV": { + "type": "string", + "example": "" + }, + "secretValueTag": { + "type": "string", + "example": "" + }, + "secretCommentCiphertext": { + "type": "string", + "example": "" + }, + "secretCommentIV": { + "type": "string", + "example": "" + }, + "secretCommentTag": { + "type": "string", + "example": "" + }, + "updatedAt": { + "type": "string", + "example": "" + }, + "createdAt": { + "type": "string", + "example": "" + } + } + }, + "Log": { + "type": "object", + "properties": { + "_id": { + "type": "string", + "example": "" + }, + "user": { + "type": "object", + "properties": { + "_id": { + "type": "string", + "example": "" + }, + "email": { + "type": "string", + "example": "" + }, + "firstName": { + "type": "string", + "example": "" + }, + "lastName": { + "type": "string", + "example": "" + } + } + }, + "workspace": { + "type": "string", + "example": "" + }, + "actionNames": { + "type": "array", + "example": [ + "addSecrets" + ], + "items": { + "type": "string" + } + }, + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "addSecrets" + }, + "user": { + "type": "string", + "example": "" + }, + "workspace": { + "type": "string", + "example": "" + }, + "payload": { + "type": "array", + "items": { + "type": "object", + "properties": { + "oldSecretVersion": { + "type": "string", + "example": "" + }, + "newSecretVersion": { + "type": "string", + "example": "" + } + } + } + } + } + } + }, + "channel": { + "type": "string", + "example": "cli" + }, + "ipAddress": { + "type": "string", + "example": "192.168.0.1" + }, + "updatedAt": { + "type": "string", + "example": "" + }, + "createdAt": { + "type": "string", + "example": "" + } + } + }, + "SecretSnapshot": { + "type": "object", + "properties": { + "workspace": { + "type": "string", + "example": "" + }, + "version": { + "type": "number", + "example": 1 + }, + "secretVersions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "_id": { + "type": "string", + "example": "" + } + } + } + } + } + }, + "SecretVersion": { + "type": "object", + "properties": { + "_id": { + "type": "string", + "example": "" + }, + "secret": { + "type": "string", + "example": "" + }, + "version": { + "type": "number", + "example": 1 + }, + "workspace": { + "type": "string", + "example": "" + }, + "type": { + "type": "string", + "example": "" + }, + "user": { + "type": "string", + "example": "" + }, + "environment": { + "type": "string", + "example": "" + }, + "isDeleted": { + "type": "string", + "example": "" + }, + "secretKeyCiphertext": { + "type": "string", + "example": "" + }, + "secretKeyIV": { + "type": "string", + "example": "" + }, + "secretKeyTag": { + "type": "string", + "example": "" + }, + "secretValueCiphertext": { + "type": "string", + "example": "" + }, + "secretValueIV": { + "type": "string", + "example": "" + }, + "secretValueTag": { + "type": "string", + "example": "" + } + } + } + }, "securitySchemes": { "bearerAuth": { "type": "http", "scheme": "bearer", - "bearerFormat": "JWT" + "bearerFormat": "JWT", + "description": "This security definition uses the HTTP 'bearer' scheme, which allows the client to authenticate using a JSON Web Token (JWT) that is passed in the Authorization header of the request." + }, + "apiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key", + "description": "This security definition uses an API key, which is passed in the header of the request as the value of the \"X-API-Key\" header. The client must provide a valid key in order to access the API." } } } diff --git a/backend/src/app.ts b/backend/src/app.ts index 82275b9c9..d32e83be7 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -8,7 +8,7 @@ import cookieParser from 'cookie-parser'; import dotenv from 'dotenv'; import swaggerUi = require('swagger-ui-express'); // eslint-disable-next-line @typescript-eslint/no-var-requires -const swaggerFile = require('../api-documentation.json') +const swaggerFile = require('../spec.json') dotenv.config(); @@ -41,7 +41,8 @@ import { integrationAuth as v1IntegrationAuthRouter } from './routes/v1'; import { - secret as v2SecretRouter, + users as v2UsersRouter, + secret as v2SecretRouter, // begin to phase out secrets as v2SecretsRouter, workspace as v2WorkspaceRouter, serviceTokenData as v2ServiceTokenDataRouter, @@ -104,6 +105,7 @@ app.use('/api/v1/integration', v1IntegrationRouter); app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); // v2 routes +app.use('/api/v2/users', v2UsersRouter); app.use('/api/v2/workspace', v2EnvironmentRouter); app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index defd03d8a..882db688f 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -170,10 +170,11 @@ export const logout = async (req: Request, res: Response) => { * @param res * @returns */ -export const checkAuth = async (req: Request, res: Response) => - res.status(200).send({ +export const checkAuth = async (req: Request, res: Response) => { + return res.status(200).send({ message: 'Authenticated' }); +} /** * Return new token by redeeming refresh token diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index 601314a2a..b2b245d9c 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -1,3 +1,4 @@ +import * as usersController from './usersController'; import * as workspaceController from './workspaceController'; import * as serviceTokenDataController from './serviceTokenDataController'; import * as apiKeyDataController from './apiKeyDataController'; @@ -6,6 +7,7 @@ import * as secretsController from './secretsController'; import * as environmentController from './environmentController'; export { + usersController, workspaceController, serviceTokenDataController, apiKeyDataController, diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 098cfed6d..77a318fc6 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -23,6 +23,58 @@ import { BadRequestError } from '../../utils/errors'; * @param res */ export const createSecrets = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Create new secret(s)' + #swagger.description = 'Create one or many secrets for a given project and environment.' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.requestBody = { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "description": "ID of project", + }, + "environment": { + "type": "string", + "description": "Environment within project" + }, + "secrets": { + $ref: "#/components/schemas/CreateSecret", + "description": "Secret(s) to create - object or array of objects" + } + } + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secrets": { + "type": "array", + "items": { + $ref: "#/components/schemas/Secret" + }, + "description": "Newly-created secrets for the given project and environment" + } + } + } + } + } + } + */ const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; const { workspaceId, environment } = req.body; @@ -161,6 +213,45 @@ export const createSecrets = async (req: Request, res: Response) => { * @returns */ export const getSecrets = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Read secrets' + #swagger.description = 'Read secrets from a project and environment' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['workspaceId'] = { + "description": "ID of project", + "required": true, + "type": "string" + } + + #swagger.parameters['environment'] = { + "description": "Environment within project", + "required": true, + "type": "string" + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secrets": { + "type": "array", + "items": { + $ref: "#/components/schemas/Secret" + }, + "description": "Secrets for the given project and environment" + } + } + } + } + } + } + */ const { workspaceId, environment } = req.query; let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user @@ -231,6 +322,50 @@ export const getSecrets = async (req: Request, res: Response) => { * @param res */ export const updateSecrets = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Update secret(s)' + #swagger.description = 'Update secret(s)' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.requestBody = { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secrets": { + $ref: "#/components/schemas/UpdateSecret", + "description": "Secret(s) to update - object or array of objects" + } + } + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secrets": { + "type": "array", + "items": { + $ref: "#/components/schemas/Secret" + }, + "description": "Updated secrets" + } + } + } + } + } + } + */ const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; // TODO: move type @@ -403,6 +538,50 @@ export const updateSecrets = async (req: Request, res: Response) => { * @param res */ export const deleteSecrets = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Delete secret(s)' + #swagger.description = 'Delete one or many secrets by their ID(s)' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.requestBody = { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secretIds": { + "type": "string", + "description": "ID(s) of secrets - string or array of strings" + }, + } + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secrets": { + "type": "array", + "items": { + $ref: "#/components/schemas/Secret" + }, + "description": "Deleted secrets" + } + } + } + } + } + } + */ const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; const toDelete = req.secrets.map((s: any) => s._id); diff --git a/backend/src/controllers/v2/usersController.ts b/backend/src/controllers/v2/usersController.ts new file mode 100644 index 000000000..7ad247ff0 --- /dev/null +++ b/backend/src/controllers/v2/usersController.ts @@ -0,0 +1,49 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { + User +} from '../../models'; + +export const getMe = async (req: Request, res: Response) => { + /* + #swagger.summary = "Retrieve the current user on the request" + #swagger.description = "Retrieve the current user on the request" + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "user": { + "type": "object", + $ref: "#/components/schemas/CurrentUser", + "description": "Current user on request" + } + } + } + } + } + } + */ + let user; + try { + user = await User + .findById(req.user._id) + .select('+publicKey +encryptedPrivateKey +iv +tag'); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get user' + }); + } + + return res.status(200).send({ + user + }); +} \ No newline at end of file diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index efac159a5..39a1be5fd 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -1,7 +1,9 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; import { Workspace, + Secret, Membership, MembershipOrg, Integration, @@ -174,6 +176,34 @@ export const pullSecrets = async (req: Request, res: Response) => { }; export const getWorkspaceKey = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Return encrypted project key' + #swagger.description = 'Return encrypted project key' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['workspaceId'] = { + "description": "ID of project", + "required": true, + "type": "string" + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "array", + "items": { + $ref: "#/components/schemas/ProjectKey" + }, + "description": "Encrypted project key for the given project" + } + } + } + } + */ let key; try { const { workspaceId } = req.params; @@ -219,4 +249,222 @@ export const getWorkspaceServiceTokenData = async ( return res.status(200).send({ serviceTokenData }); +} + +/** + * Return memberships for workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ +export const getWorkspaceMemberships = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Return project memberships' + #swagger.description = 'Return project memberships' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['workspaceId'] = { + "description": "ID of project", + "required": true, + "type": "string" + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "memberships": { + "type": "array", + "items": { + $ref: "#/components/schemas/Membership" + }, + "description": "Memberships of project" + } + } + } + } + } + } + */ + let memberships; + try { + const { workspaceId } = req.params; + + memberships = await Membership.find({ + workspace: workspaceId + }).populate('user', '+publicKey'); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace memberships' + }); + } + + return res.status(200).send({ + memberships + }); +} + +/** + * Delete workspace membership with id [membershipId] + * @param req + * @param res + * @returns + */ +export const deleteWorkspaceMembership = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Delete project membership' + #swagger.description = 'Delete project membership' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['workspaceId'] = { + "description": "ID of project", + "required": true, + "type": "string" + } + + #swagger.parameters['membershipId'] = { + "description": "ID of membership", + "required": true, + "type": "string" + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "membership": { + $ref: "#/components/schemas/Membership", + "description": "Deleted membership" + } + } + } + } + } + } + */ + let membership; + try { + const { + membershipId + } = req.params; + + membership = await Membership.findByIdAndDelete(membershipId); + + if (!membership) throw new Error('Failed to delete workspace membership'); + + await Key.deleteMany({ + receiver: membership.user, + workspace: membership.workspace + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to delete workspace membership' + }); + } + + return res.status(200).send({ + membership + }); +} + +/** + * Update role of membership with id [membershipId] to role [role] + * @param req + * @param res + * @returns + */ +export const updateWorkspaceMembership = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Update project membership' + #swagger.description = 'Update project membership' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['workspaceId'] = { + "description": "ID of project", + "required": true, + "type": "string" + } + + #swagger.parameters['membershipId'] = { + "description": "ID of membership", + "required": true, + "type": "string" + } + + #swagger.requestBody = { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": "Role of membership - either admin or member", + } + } + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "membership": { + $ref: "#/components/schemas/Membership", + "description": "Updated membership" + } + } + } + } + } + } + */ + let membership; + try { + const { + membershipId + } = req.params; + const { role } = req.body; + + membership = await Membership.findByIdAndUpdate( + membershipId, + { + role + }, { + new: true + } + ); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to update workspace membership' + }); + } + + return res.status(200).send({ + membership + }); } \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts index 751f21611..562c8aa88 100644 --- a/backend/src/ee/controllers/v1/secretController.ts +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -10,6 +10,51 @@ import { EESecretService } from '../../services'; * @param res */ export const getSecretVersions = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Return secret versions' + #swagger.description = 'Return secret versions' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['secretId'] = { + "description": "ID of secret", + "required": true, + "type": "string" + } + + #swagger.parameters['offset'] = { + "description": "Number of versions to skip", + "required": false, + "type": "string" + } + + #swagger.parameters['limit'] = { + "description": "Maximum number of versions to return", + "required": false, + "type": "string" + } + + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + "type": "object", + "properties": { + "secretVersions": { + "type": "array", + "items": { + $ref: "#/components/schemas/SecretVersion" + }, + "description": "Secret versions" + } + } + } + } + } + } + */ let secretVersions; try { const { secretId } = req.params; @@ -44,6 +89,54 @@ import { EESecretService } from '../../services'; * @returns */ export const rollbackSecretVersion = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Roll back secret to a version.' + #swagger.description = 'Roll back secret to a version.' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['secretId'] = { + "description": "ID of secret", + "required": true, + "type": "string" + } + + #swagger.requestBody = { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "version": { + "type": "integer", + "description": "Version of secret to roll back to" + } + } + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + "type": "object", + "properties": { + "secret": { + "type": "object", + $ref: "#/components/schemas/Secret", + "description": "Secret rolled back to" + } + } + } + } + } + } + */ let secret; try { const { secretId } = req.params; diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 8fd7c8746..ea9bb7dab 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -19,6 +19,51 @@ import { getLatestSecretVersionIds } from '../../helpers/secretVersion'; * @param res */ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Return project secret snapshot ids' + #swagger.description = 'Return project secret snapshots ids' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['workspaceId'] = { + "description": "ID of project", + "required": true, + "type": "string" + } + + #swagger.parameters['offset'] = { + "description": "Number of secret snapshots to skip", + "required": false, + "type": "string" + } + + #swagger.parameters['limit'] = { + "description": "Maximum number of secret snapshots to return", + "required": false, + "type": "string" + } + + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + "type": "object", + "properties": { + "secretSnapshots": { + "type": "array", + "items": { + $ref: "#/components/schemas/SecretSnapshot" + }, + "description": "Project secret snapshots" + } + } + } + } + } + } + */ let secretSnapshots; try { const { workspaceId } = req.params; @@ -78,16 +123,66 @@ export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Respon * @returns */ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Roll back project secrets to those captured in a secret snapshot version.' + #swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['workspaceId'] = { + "description": "ID of project", + "required": true, + "type": "string" + } + + #swagger.requestBody = { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "version": { + "type": "integer", + "description": "Version of secret snapshot to roll back to", + } + } + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + "type": "object", + "properties": { + "secrets": { + "type": "array", + "items": { + $ref: "#/components/schemas/Secret" + }, + "description": "Secrets rolled back to" + } + } + } + } + } + } + */ let secrets; try { const { workspaceId } = req.params; const { version } = req.body; // validate secret snapshot - const secretSnapshot = await SecretSnapshot.findOne({ - workspace: workspaceId, - version - }).populate<{ secretVersions: ISecretVersion[]}>('secretVersions'); + const secretSnapshot = await SecretSnapshot.findOne({ + workspace: workspaceId, + version + }).populate<{ secretVersions: ISecretVersion[]}>('secretVersions'); if (!secretSnapshot) throw new Error('Failed to find secret snapshot'); @@ -231,6 +326,72 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons * @returns */ export const getWorkspaceLogs = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Return project (audit) logs' + #swagger.description = 'Return project (audit) logs' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['workspaceId'] = { + "description": "ID of project", + "required": true, + "type": "string" + } + + #swagger.parameters['userId'] = { + "description": "ID of project member", + "required": false, + "type": "string" + } + + #swagger.parameters['offset'] = { + "description": "Number of logs to skip", + "required": false, + "type": "string" + } + + #swagger.parameters['limit'] = { + "description": "Maximum number of logs to return", + "required": false, + "type": "string" + } + + #swagger.parameters['sortBy'] = { + "description": "Order to sort the logs by", + "schema": { + "type": "string", + "@enum": ["oldest", "recent"] + }, + "required": false + } + + #swagger.parameters['actionNames'] = { + "description": "Names of log actions (comma-separated)", + "required": false, + "type": "string" + } + + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + "type": "object", + "properties": { + "logs": { + "type": "array", + "items": { + $ref: "#/components/schemas/Log" + }, + "description": "Project logs" + } + } + } + } + } + } + */ let logs try { const { workspaceId } = req.params; diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index 6a3537076..e9bffac2d 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -2,6 +2,7 @@ import requireAuth from './requireAuth'; import requireBotAuth from './requireBotAuth'; import requireSignupAuth from './requireSignupAuth'; import requireWorkspaceAuth from './requireWorkspaceAuth'; +import requireMembershipAuth from './requireMembershipAuth'; import requireOrganizationAuth from './requireOrganizationAuth'; import requireIntegrationAuth from './requireIntegrationAuth'; import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth'; @@ -16,6 +17,7 @@ export { requireBotAuth, requireSignupAuth, requireWorkspaceAuth, + requireMembershipAuth, requireOrganizationAuth, requireIntegrationAuth, requireIntegrationAuthorizationAuth, diff --git a/backend/src/middleware/requireMembershipAuth.ts b/backend/src/middleware/requireMembershipAuth.ts new file mode 100644 index 000000000..06c0980a3 --- /dev/null +++ b/backend/src/middleware/requireMembershipAuth.ts @@ -0,0 +1,58 @@ +import { Request, Response, NextFunction } from 'express'; +import { UnauthorizedRequestError } from '../utils/errors'; +import { + Membership, +} from '../models'; +import { validateMembership } from '../helpers/membership'; + +type req = 'params' | 'body' | 'query'; +/** + * Validate membership with id [membershipId] and that user with id + * [req.user._id] can modify that membership. + * @param {Object} obj + * @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth + * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing + */ +const requireMembershipAuth = ({ + acceptedRoles, + location = 'params' +}: { + acceptedRoles: string[]; + location?: req; +}) => { + return async ( + req: Request, + res: Response, + next: NextFunction + ) => { + try { + const { membershipId } = req[location]; + + const membership = await Membership.findById(membershipId); + + if (!membership) throw new Error('Failed to find target membership'); + + const userMembership = await Membership.findOne({ + workspace: membership.workspace + }); + + if (!userMembership) throw new Error('Failed to validate own membership') + + const targetMembership = await validateMembership({ + userId: req.user._id.toString(), + workspaceId: membership.workspace.toString(), + acceptedRoles + }); + + req.targetMembership = targetMembership; + + return next(); + } catch (err) { + return next(UnauthorizedRequestError({ + message: 'Unable to validate workspace membership' + })); + } + } +} + +export default requireMembershipAuth; \ No newline at end of file diff --git a/backend/src/routes/v1/membership.ts b/backend/src/routes/v1/membership.ts index 76bf3e57a..f2c3eb554 100644 --- a/backend/src/routes/v1/membership.ts +++ b/backend/src/routes/v1/membership.ts @@ -4,7 +4,7 @@ import { body, param } from 'express-validator'; import { requireAuth, validateRequest } from '../../middleware'; import { membershipController } from '../../controllers/v1'; -router.get( // used for CLI (deprecate) +router.get( // used for old CLI (deprecate) '/:workspaceId/connect', requireAuth({ acceptedAuthModes: ['jwt'] diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts index 2740c3530..cc9375fb7 100644 --- a/backend/src/routes/v2/index.ts +++ b/backend/src/routes/v2/index.ts @@ -1,3 +1,4 @@ +import users from './users'; import secret from './secret'; // stop-supporting import secrets from './secrets'; import workspace from './workspace'; @@ -6,6 +7,7 @@ import apiKeyData from './apiKeyData'; import environment from "./environment" export { + users, secret, secrets, workspace, diff --git a/backend/src/routes/v2/users.ts b/backend/src/routes/v2/users.ts new file mode 100644 index 000000000..dba107b15 --- /dev/null +++ b/backend/src/routes/v2/users.ts @@ -0,0 +1,16 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth +} from '../../middleware'; +import { usersController } from '../../controllers/v2'; + +router.get( + '/me', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + usersController.getMe +); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index c90834d6d..ca920e15a 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -3,6 +3,7 @@ const router = express.Router(); import { body, param, query } from 'express-validator'; import { requireAuth, + requireMembershipAuth, requireWorkspaceAuth, validateRequest } from '../../middleware'; @@ -67,4 +68,54 @@ router.get( workspaceController.getWorkspaceServiceTokenData ); +// TODO: /POST to create membership and re-route inviting user to workspace there + +router.get( // new - TODO: rewire dashboard to this route + '/:workspaceId/memberships', + param('workspaceId').exists().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + }), + workspaceController.getWorkspaceMemberships +); + +router.delete( // TODO - rewire dashboard to this route + '/:workspaceId/memberships/:membershipId', + param('workspaceId').exists().trim(), + param('membershipId').exists().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN], + }), + requireMembershipAuth({ + acceptedRoles: [ADMIN] + }), + workspaceController.deleteWorkspaceMembership +); + +router.patch( // TODO - rewire dashboard to this route + '/:workspaceId/memberships/:membershipId', + param('workspaceId').exists().trim(), + param('membershipId').exists().trim(), + body('role').exists().isString().trim().isIn([ADMIN, MEMBER]), + validateRequest, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN], + }), + requireMembershipAuth({ + acceptedRoles: [ADMIN] + }), + workspaceController.updateWorkspaceMembership +); + export default router; diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index ae9edb4c5..1ce63e609 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -8,6 +8,7 @@ declare global { user: any; workspace: any; membership: any; + targetMembership: any; organization: any; membershipOrg: any; integration: any; diff --git a/backend/swagger.ts b/backend/swagger.ts deleted file mode 100644 index 0505bb813..000000000 --- a/backend/swagger.ts +++ /dev/null @@ -1,22 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-var-requires -const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' }); - -const doc = { - info: { - title: 'Infisical API', - description: 'List of all available APIs that can be consumed', - }, - host: ['https://infisical.com'], - securityDefinitions: { - bearerAuth: { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT' - } - } -}; - -const outputFile = './api-documentation.json'; -const endpointsFiles = ['./src/app.ts']; - -swaggerAutogen(outputFile, endpointsFiles, doc); \ No newline at end of file diff --git a/backend/swagger/index.ts b/backend/swagger/index.ts new file mode 100644 index 000000000..cbf05ed7e --- /dev/null +++ b/backend/swagger/index.ts @@ -0,0 +1,186 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' }); +const fs = require('fs').promises; +const yaml = require('js-yaml'); +const { secretSchema } = require('./schemas/index.ts'); + +/** + * Generates OpenAPI specs for all Infisical API endpoints: + * - spec.json in /backend for api-serving + * - spec.yaml in /docs for API reference + */ +const generateOpenAPISpec = async () => { + const doc = { + info: { + title: 'Infisical API', + description: 'List of all available APIs that can be consumed', + }, + host: ['https://infisical.com'], + servers: [ + { + url: 'https://infisical.com', + description: 'Production server' + }, + { + url: 'http://localhost:8080', + description: 'Local server' + } + ], + securityDefinitions: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: "This security definition uses the HTTP 'bearer' scheme, which allows the client to authenticate using a JSON Web Token (JWT) that is passed in the Authorization header of the request." + }, + apiKeyAuth: { + type: 'apiKey', + in: 'header', + name: 'X-API-Key', + description: 'This security definition uses an API key, which is passed in the header of the request as the value of the "X-API-Key" header. The client must provide a valid key in order to access the API.' + } + }, + definitions: { + CurrentUser: { + _id: '', + email: '', + firstName: '', + lastName: '', + publicKey: '', + encryptedPrivateKey: '', + iv: '', + tag: '', + updatedAt: '', + createdAt: '' + }, + Membership: { + user: { + _id: '', + email: '', + firstName: '', + lastName: '', + publicKey: '', + updatedAt: '', + createdAt: '' + }, + workspace: '', + role: 'admin' + }, + ProjectKey: { + encryptedkey: '', + nonce: '', + sender: { + publicKey: '' + }, + receiver: '', + workspace: '' + }, + CreateSecret: { + type: 'shared', + secretKeyCiphertext: '', + secretKeyIV: '', + secretKeyTag: '', + secretValueCiphertext: '', + secretValueIV: '', + secretValueTag: '', + secretCommentCiphertext: '', + secretCommentIV: '', + secretCommentTag: '' + }, + UpdateSecret: { + id: '', + secretKeyCiphertext: '', + secretKeyIV: '', + secretKeyTag: '', + secretValueCiphertext: '', + secretValueIV: '', + secretValueTag: '', + secretCommentCiphertext: '', + secretCommentIV: '', + secretCommentTag: '' + }, + Secret: { + _id: '', + version: 1, + workspace : '', + type: 'shared', + user: null, + secretKeyCiphertext: '', + secretKeyIV: '', + secretKeyTag: '', + secretValueCiphertext: '', + secretValueIV: '', + secretValueTag: '', + secretCommentCiphertext: '', + secretCommentIV: '', + secretCommentTag: '', + updatedAt: '', + createdAt: '' + }, + Log: { + _id: '', + user: { + _id: '', + email: '', + firstName: '', + lastName: '' + }, + workspace: '', + actionNames: [ + 'addSecrets' + ], + actions: [ + { + name: 'addSecrets', + user: '', + workspace: '', + payload: [ + { + oldSecretVersion: '', + newSecretVersion: '' + } + ] + } + ], + channel: 'cli', + ipAddress: '192.168.0.1', + updatedAt: '', + createdAt: '' + }, + SecretSnapshot: { + workspace: '', + version: 1, + secretVersions: [ + { + _id: '' + } + ] + }, + SecretVersion: { + _id: '', + secret: '', + version: 1, + workspace: '', + type: '', + user: '', + environment: '', + isDeleted: '', + secretKeyCiphertext: '', + secretKeyIV: '', + secretKeyTag: '', + secretValueCiphertext: '', + secretValueIV: '', + secretValueTag: '', + } + } + }; + + const outputJSONFile = '../spec.json'; + const outputYAMLFile = '../docs/spec.yaml'; + const endpointsFiles = ['../src/app.ts']; + + const spec = await swaggerAutogen(outputJSONFile, endpointsFiles, doc); + await fs.writeFile(outputYAMLFile, yaml.dump(spec.data)); +} + +generateOpenAPISpec(); diff --git a/backend/swagger/schemas/index.ts b/backend/swagger/schemas/index.ts new file mode 100644 index 000000000..b0b427fce --- /dev/null +++ b/backend/swagger/schemas/index.ts @@ -0,0 +1,6 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const secretSchema = require('./secretSchema.ts'); + +module.exports = { + secretSchema +} \ No newline at end of file diff --git a/backend/swagger/schemas/secretSchema.ts b/backend/swagger/schemas/secretSchema.ts new file mode 100644 index 000000000..85c6ce017 --- /dev/null +++ b/backend/swagger/schemas/secretSchema.ts @@ -0,0 +1,11 @@ +const secretSchema = { + _id: { + type: 'string', + format: 'objectId' + }, + version: { + type: 'number' + } +} + +module.exports = secretSchema; \ No newline at end of file diff --git a/docs/api-reference/endpoints/secrets/read.mdx b/docs/api-reference/endpoints/secrets/read.mdx index 4305f192c..c88fb0ec7 100644 --- a/docs/api-reference/endpoints/secrets/read.mdx +++ b/docs/api-reference/endpoints/secrets/read.mdx @@ -1,4 +1,4 @@ --- -title: "Read" +title: "Retrieve" openapi: "GET /api/v2/secrets/" --- diff --git a/docs/api-reference/endpoints/secrets/rollback-version.mdx b/docs/api-reference/endpoints/secrets/rollback-version.mdx new file mode 100644 index 000000000..dff577fe1 --- /dev/null +++ b/docs/api-reference/endpoints/secrets/rollback-version.mdx @@ -0,0 +1,4 @@ +--- +title: "Roll Back to Version" +openapi: "POST /api/v1/secret/{secretId}/secret-versions/rollback" +--- diff --git a/docs/api-reference/endpoints/secrets/versions.mdx b/docs/api-reference/endpoints/secrets/versions.mdx new file mode 100644 index 000000000..e8693684c --- /dev/null +++ b/docs/api-reference/endpoints/secrets/versions.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Versions" +openapi: "GET /api/v1/secret/{secretId}/secret-versions" +--- diff --git a/docs/api-reference/endpoints/users/me.mdx b/docs/api-reference/endpoints/users/me.mdx new file mode 100644 index 000000000..9273f0ca6 --- /dev/null +++ b/docs/api-reference/endpoints/users/me.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Current User" +openapi: "GET /api/v2/users/me" +--- diff --git a/docs/api-reference/endpoints/workspaces/delete-membership.mdx b/docs/api-reference/endpoints/workspaces/delete-membership.mdx new file mode 100644 index 000000000..826eafa83 --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/delete-membership.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete Membership" +openapi: "DELETE /api/v2/workspace/{workspaceId}/memberships/{membershipId}" +--- diff --git a/docs/api-reference/endpoints/workspaces/logs.mdx b/docs/api-reference/endpoints/workspaces/logs.mdx new file mode 100644 index 000000000..d5921c1c0 --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/logs.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Logs" +openapi: "GET /api/v1/workspace/{workspaceId}/logs" +--- diff --git a/docs/api-reference/endpoints/workspaces/memberships.mdx b/docs/api-reference/endpoints/workspaces/memberships.mdx new file mode 100644 index 000000000..38b86d616 --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/memberships.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Memberships" +openapi: "GET /api/v2/workspace/{workspaceId}/memberships" +--- diff --git a/docs/api-reference/endpoints/workspaces/rollback-snapshot.mdx b/docs/api-reference/endpoints/workspaces/rollback-snapshot.mdx new file mode 100644 index 000000000..8b648a400 --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/rollback-snapshot.mdx @@ -0,0 +1,4 @@ +--- +title: "Roll Back to Snapshot" +openapi: "POST /api/v1/workspace/{workspaceId}/secret-snapshots/rollback" +--- diff --git a/docs/api-reference/endpoints/workspaces/secret-snapshots.mdx b/docs/api-reference/endpoints/workspaces/secret-snapshots.mdx new file mode 100644 index 000000000..2d645d4d0 --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/secret-snapshots.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Snapshots" +openapi: "GET /api/v1/workspace/{workspaceId}/secret-snapshots" +--- diff --git a/docs/api-reference/endpoints/workspaces/update-membership.mdx b/docs/api-reference/endpoints/workspaces/update-membership.mdx new file mode 100644 index 000000000..367793847 --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/update-membership.mdx @@ -0,0 +1,4 @@ +--- +title: "Update Membership" +openapi: "PATCH /api/v2/workspace/{workspaceId}/memberships/{membershipId}" +--- diff --git a/docs/api-reference/endpoints/workspaces/workspace-key.mdx b/docs/api-reference/endpoints/workspaces/workspace-key.mdx new file mode 100644 index 000000000..05313eb8f --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/workspace-key.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Key" +openapi: "GET /api/v2/workspace/{workspaceId}/encrypted-key" +--- diff --git a/docs/api-reference/overview/authentication.mdx b/docs/api-reference/overview/authentication.mdx index 27a2dc134..8cc218253 100644 --- a/docs/api-reference/overview/authentication.mdx +++ b/docs/api-reference/overview/authentication.mdx @@ -1,3 +1,11 @@ --- title: "Authentication" --- + +To authenticate requests with Infisical, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. You can obtain an API key from your user settings. + + + It's important to keep your API key secure, as it grants access to your + secrets in Infisical. For added security, consider rotating your API key on a + regular basis. + diff --git a/docs/api-reference/overview/examples/create-secrets.mdx b/docs/api-reference/overview/examples/create-secrets.mdx new file mode 100644 index 000000000..b2afe1467 --- /dev/null +++ b/docs/api-reference/overview/examples/create-secrets.mdx @@ -0,0 +1,152 @@ +--- +title: "Create secrets" +--- + +In this example, we demonstrate how to add secrets to a project and environment. + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) +- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). + +## Flow + +1. Get your (encrypted) private key. +2. Decrypt your (encrypted) private key with your password. +3. Get the (encrypted) project key for the project. +4. Decrypt the (encrypted) project key with your private key. +5. Encrypt your secret(s) with the project key. +6. Send (encrypted) secret(s) to the Infical API + +## Example + +```js +const crypto = require('crypto'); +const axios = require('axios'); + +const ALGORITHM = 'aes-256-gcm'; +const BLOCK_SIZE_BYTES = 16; + +const encrypt = ( + text, + secret +) => { + const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); + const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); + + let ciphertext = cipher.update(text, 'utf8', 'base64'); + ciphertext += cipher.final('base64'); + return { + ciphertext, + iv: iv.toString('base64'), + tag: cipher.getAuthTag().toString('base64') + }; +} + +const decrypt = (ciphertext, iv, tag, secret) => { + const 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 createSecrets = async () => { + const API_KEY = 'your_api_key'; + const PSWD = 'your_pswd'; + const WORKSPACE_ID = 'your_workspace_id'; + + const SECRET_KEY = 'SOME_KEY'; + const SECRET_VALUE = 'SOME_VALUE'; + + // 1. get (encrypted) private key + const user = await axios.get( + 'https://api.infisical.com/api/v2/users/me', { + headers: { + 'X-API-KEY': API_KEY + } + } + ); + + // 2. decrypt your (encrypted) private key with your password + const privateKey = decrypt({ + ciphertext: user.encryptedPrivateKey, + iv: user.iv, + tag: user.tag, + secret: PSWD.slice(0, 32).padStart(32, '0'); + }); + + // 3. get the (encrypted) project key for the project + const encryptedProjectKey = await axios.get( + `https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, { + headers: { + 'X-API-KEY': API_KEY + } + } + ); + + // 4. decrypt the project key with your private key + const projectKey = nacl.box.open( + util.decodeBase64(encryptedProjectKey), + util.decodeBase64(projectKey.nonce), + util.decodeBase64(projectKey.sender.publicKey), + util.decodeBase64(privateKey) + ); + + // 5. encrypt your secret(s) with the project key + const { + ciphertext: secretKeyCiphertext, + iv: secretKeyIV, + tag: secretKeyTag + } = encrypt(SECRET_KEY, projectKey); + + const { + ciphertext: secretValueCiphertext, + iv: secretValueIV, + tag: secretValueTag + } = encrypt(SECRET_VALUE, projectKey); + + const secret = { + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag + } + + // 6. Send (encrypted) secret(s) to the Infisical API + await axios.post( + `https://api.infisical.com/api/v2/secrets`, + { + workspaceId: WORKSPACE_ID, + environment: 'dev', + secrets: secret + }, + { + headers: { + 'X-API-KEY': API_KEY + } + } + ); +} + +createSecrets(); +``` + + + 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. + + + It can be useful to perform steps 1-4 ahead of time and store away your + private key (and even project key) for later use. The Infisical CLI works by + securely storing your private key via your OS keyring. + diff --git a/docs/api-reference/overview/examples/delete-secrets.mdx b/docs/api-reference/overview/examples/delete-secrets.mdx new file mode 100644 index 000000000..c602cef7e --- /dev/null +++ b/docs/api-reference/overview/examples/delete-secrets.mdx @@ -0,0 +1,34 @@ +--- +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) +- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). + +## Example + +```js +const deleteSecrets = async () => { + const API_KEY = "your_api_key"; + const SECRET_ID = "ID"; // ID of secret to delete + + // 6. Send ID(s) of secret(s) to delete to the Infisical API + await axios.delete( + `https://api.infisical.com/api/v2/secrets`, + { + secretIds: SECRET_ID, + }, + { + headers: { + "X-API-KEY": API_KEY, + }, + } + ); +}; + +deleteSecrets(); +``` diff --git a/docs/api-reference/overview/examples/retrieve-secrets.mdx b/docs/api-reference/overview/examples/retrieve-secrets.mdx new file mode 100644 index 000000000..2f34339c5 --- /dev/null +++ b/docs/api-reference/overview/examples/retrieve-secrets.mdx @@ -0,0 +1,142 @@ +--- +title: "Retrieve secrets" +--- + +In this example, we demonstrate how to retrieve secrets from a project and environment. + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) +- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). + +## Flow + +1. Get your (encrypted) private key. +2. Decrypt your (encrypted) private key with your password. +3. Get the (encrypted) project key for the project. +4. Decrypt the (encrypted) project key with your private key. +5. Get secrets for a project and environment. +6. Decrypt the (encrypted) secrets + +## Example + +```js +const crypto = require('crypto'); +const axios = require('axios'); + +const ALGORITHM = 'aes-256-gcm'; +const BLOCK_SIZE_BYTES = 16; + +const encrypt = ( + text, + secret +) => { + const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); + const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); + + let ciphertext = cipher.update(text, 'utf8', 'base64'); + ciphertext += cipher.final('base64'); + return { + ciphertext, + iv: iv.toString('base64'), + tag: cipher.getAuthTag().toString('base64') + }; +} + +const decrypt = (ciphertext, iv, tag, secret) => { + const 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 retrieveSecrets = async () => { + const API_KEY = 'your_api_key'; + const PSWD = 'your_pswd'; + const WORKSPACE_ID = 'your_workspace_id'; + + // 1. get (encrypted) private key + const user = await axios.get( + 'https://api.infisical.com/api/v2/users/me', { + headers: { + 'X-API-KEY': API_KEY + } + } + ); + + // 2. decrypt your (encrypted) private key with your password + const privateKey = decrypt({ + ciphertext: user.encryptedPrivateKey, + iv: user.iv, + tag: user.tag, + secret: PSWD.slice(0, 32).padStart(32, '0'); + }); + + // 3. get the (encrypted) project key for the project + const encryptedProjectKey = await axios.get( + `https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, { + headers: { + 'X-API-KEY': API_KEY + } + } + ); + + // 4. decrypt the project key with your private key + const projectKey = nacl.box.open( + util.decodeBase64(encryptedProjectKey), + util.decodeBase64(projectKey.nonce), + util.decodeBase64(projectKey.sender.publicKey), + util.decodeBase64(privateKey) + ); + + // 5. get (encrypted) secrets for a project and environment. + const encryptedSecrets = await axios.get( + 'https://api.infisical.com/api/v2/secrets', { + headers: { + 'X-API-KEY': API_KEY + } + } + ); + + // 6. decrypt the (encrypted) secrets + const secrets = encryptedSecrets.map((encryptedSecret) => { + const secretKey = decrypt({ + ciphertext: encryptedSecret.secretKeyCiphertext, + iv: encryptedSecret.secretKeyIV, + tag: encryptedSecret.secretKeyTag + secret: projectKey + }); + const secretValue = decrypt({ + ciphertext: encryptedSecret.secretValueCiphertext, + iv: encryptedSecret.secretValueIV, + tag: encryptedSecret.secretValueTag + secret: projectKey + }); + + return ({ + secretKey, + secretValue + }); + }); +} + +retrieveSecrets(); +``` + + + 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. + + + It can be useful to perform steps 1-4 ahead of time and store away your + private key (and even project key) for later use. The Infisical CLI works by + securely storing your private key via your OS keyring. + diff --git a/docs/api-reference/overview/examples/update-secrets.mdx b/docs/api-reference/overview/examples/update-secrets.mdx new file mode 100644 index 000000000..b8566d9b3 --- /dev/null +++ b/docs/api-reference/overview/examples/update-secrets.mdx @@ -0,0 +1,152 @@ +--- +title: "Update secrets" +--- + +In this example, we demonstrate how to update secrets + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) +- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). + +## Flow + +1. Get your (encrypted) private key. +2. Decrypt your (encrypted) private key with your password. +3. Get the project key for the project. +4. Decrypt the project key with your private key. +5. Encrypt your secret(s) with the project key. +6. Send (encrypted) updated secret(s) to the Infical API + +## Example + +```js +const crypto = require('crypto'); +const axios = require('axios'); + +const ALGORITHM = 'aes-256-gcm'; +const BLOCK_SIZE_BYTES = 16; + +const encrypt = ( + text, + secret +) => { + const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); + const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); + + let ciphertext = cipher.update(text, 'utf8', 'base64'); + ciphertext += cipher.final('base64'); + return { + ciphertext, + iv: iv.toString('base64'), + tag: cipher.getAuthTag().toString('base64') + }; +} + +const decrypt = (ciphertext, iv, tag, secret) => { + const 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 updateSecrets = async () => { + const API_KEY = 'your_api_key'; + const PSWD = 'your_pswd'; + const WORKSPACE_ID = 'your_workspace_id'; + + const SECRET_ID = 'ID' // ID of secret to update + const SECRET_KEY = 'SOME_KEY'; + const SECRET_VALUE = 'SOME_VALUE'; + + // 1. get (encrypted) private key + const user = await axios.get( + 'https://api.infisical.com/api/v2/users/me', { + headers: { + 'X-API-KEY': API_KEY + } + } + ); + + // 2. decrypt your (encrypted) private key with your password + const privateKey = decrypt({ + ciphertext: user.encryptedPrivateKey, + iv: user.iv, + tag: user.tag, + secret: PSWD.slice(0, 32).padStart(32, '0'); + }); + + // 3. get the (encrypted) project key for the project + const encryptedProjectKey = await axios.get( + `https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, { + headers: { + 'X-API-KEY': API_KEY + } + } + ); + + // 4. decrypt the project key with your private key + const projectKey = nacl.box.open( + util.decodeBase64(encryptedProjectKey), + util.decodeBase64(projectKey.nonce), + util.decodeBase64(projectKey.sender.publicKey), + util.decodeBase64(privateKey) + ); + + // 5. encrypt your secret(s) with the project key + const { + ciphertext: secretKeyCiphertext, + iv: secretKeyIV, + tag: secretKeyTag + } = encrypt(SECRET_KEY, projectKey); + + const { + ciphertext: secretValueCiphertext, + iv: secretValueIV, + tag: secretValueTag + } = encrypt(SECRET_VALUE, projectKey); + + const secret = { + id: SECRET_ID, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag + } + + // 6. Send (encrypted) secret(s) to the Infisical API + await axios.patch( + `https://api.infisical.com/api/v2/secrets`, + { + secrets: secret + }, + { + headers: { + 'X-API-KEY': API_KEY + } + } + ); +} + +updateSecrets(); +``` + + + 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. + + + It can be useful to perform steps 1-4 ahead of time and store away your + private key (and even project key) for later use. The Infisical CLI works by + securely storing your private key via your OS keyring. + diff --git a/docs/api-reference/overview/introduction.mdx b/docs/api-reference/overview/introduction.mdx index 9632e3788..7babef955 100644 --- a/docs/api-reference/overview/introduction.mdx +++ b/docs/api-reference/overview/introduction.mdx @@ -1,3 +1,31 @@ --- title: "Introduction" --- + + + Infisical's REST API is currently unavailable and scheduled to go live on Jan + 16! + + +Infisical's REST API provides users an alternative way to programmatically access and manage +secrets via HTTPS requests. This can be useful for automating tasks, such as +rotating credentials, or for integrating secret management into a larger system. + +With the REST API, users can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more. + +## Concepts + +Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview). + +- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by the user's password before being sent off to the server during the account signup process. +- Each (encrypted) secret belongs to a project and environment. +- Each project has an (encrypted) project key used to encrypt the secrets within that project; Infisical stores copies of the project key, for each member of that project, encrypted under each member's public key. +- Secrets are encrypted symmetrically by your copy of the project key belonging to the project containing. +- Infisical uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations. + + + Infisical's system ensures greater security such that secrets are + encrypted/decrypted on the client-side but requires users to properly + implement cryptographic operations to maintain end-to-end encryption (E2EE). + We're + diff --git a/docs/api-reference/overview/usage.mdx b/docs/api-reference/overview/usage.mdx new file mode 100644 index 000000000..9f23080c7 --- /dev/null +++ b/docs/api-reference/overview/usage.mdx @@ -0,0 +1,18 @@ +--- +title: "Usage" +--- + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) or your self-hosted instance. +- Obtain an API Key in your user settings to be included in requests to the Infisical API. + +Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview). + +## Concepts + +- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by the user's password before being sent off to the server during the account signup process. +- Each (encrypted) secret belongs to a project and environment. +- Each project has an (encrypted) project key used to encrypt the secrets within that project; Infisical stores copies of the project key, for each member of that project, encrypted under each member's public key. +- Secrets are encrypted symmetrically by your copy of the project key belonging to the project containing. +- Infisical uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations. diff --git a/docs/mint.json b/docs/mint.json index 4b83b5c17..77d17c6c3 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -21,6 +21,16 @@ "to": "#F8B7BD" } }, + "api": { + "baseUrl": [ + "https://app.infisical.com", + "http://localhost:8080" + ], + "auth": { + "method": "api-key", + "name": "X-API-KEY" + } + }, "topbarLinks": [ { "name": "Log In", "url": "https://app.infisical.com/login" } ], @@ -39,6 +49,11 @@ "icon": "server", "url": "self-hosting" }, + { + "name": "API Reference", + "icon": "cloud", + "url": "api-reference" + }, { "name": "Integrations", "icon": "plug", @@ -125,6 +140,56 @@ "self-hosting/configuration/email" ] }, + { + "group": "Overview", + "pages": [ + "api-reference/overview/introduction", + "api-reference/overview/authentication", + { + "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" + ] + } + ] + }, + { + "group": "Endpoints", + "pages": [ + { + "group": "Users", + "pages": [ + "api-reference/endpoints/users/me" + ] + }, + { + "group": "Projects", + "pages": [ + "api-reference/endpoints/workspaces/memberships", + "api-reference/endpoints/workspaces/update-membership", + "api-reference/endpoints/workspaces/delete-membership", + "api-reference/endpoints/workspaces/workspace-key", + "api-reference/endpoints/workspaces/logs", + "api-reference/endpoints/workspaces/secret-snapshots", + "api-reference/endpoints/workspaces/rollback-snapshot" + ] + }, + { + "group": "Secrets", + "pages": [ + "api-reference/endpoints/secrets/create", + "api-reference/endpoints/secrets/read", + "api-reference/endpoints/secrets/update", + "api-reference/endpoints/secrets/delete", + "api-reference/endpoints/secrets/versions", + "api-reference/endpoints/secrets/rollback-version" + ] + } + ] + }, { "group": "Integrations", "pages": ["integrations/overview"] diff --git a/docs/spec.yaml b/docs/spec.yaml new file mode 100644 index 000000000..991682871 --- /dev/null +++ b/docs/spec.yaml @@ -0,0 +1,2327 @@ +openapi: 3.0.0 +info: + title: Infisical API + description: List of all available APIs that can be consumed + version: 1.0.0 +servers: + - url: https://infisical.com + description: Production server + - url: http://localhost:8080 + description: Local server +paths: + /api/v1/secret/{secretId}/secret-versions: + get: + summary: Return secret versions + description: Return secret versions + parameters: + - name: secretId + in: path + required: true + schema: + type: string + description: ID of secret + - name: offset + description: Number of versions to skip + required: false + in: query + schema: + type: string + - name: limit + description: Maximum number of versions to return + required: false + in: query + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + secretVersions: + type: array + items: + $ref: '#/components/schemas/SecretVersion' + description: Secret versions + '400': + description: Bad Request + security: + - apiKeyAuth: [] + /api/v1/secret/{secretId}/secret-versions/rollback: + post: + summary: Roll back secret to a version. + description: Roll back secret to a version. + parameters: + - name: secretId + in: path + required: true + schema: + type: string + description: ID of secret + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + secret: + type: object + $ref: '#/components/schemas/Secret' + description: Secret rolled back to + '400': + description: Bad Request + security: + - apiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + version: + type: integer + description: Version of secret to roll back to + /api/v1/secret-snapshot/{secretSnapshotId}: + get: + description: '' + parameters: + - name: secretSnapshotId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/workspace/{workspaceId}/secret-snapshots: + get: + summary: Return project secret snapshot ids + description: Return project secret snapshots ids + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + description: ID of project + - name: offset + description: Number of secret snapshots to skip + required: false + in: query + schema: + type: string + - name: limit + description: Maximum number of secret snapshots to return + required: false + in: query + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + secretSnapshots: + type: array + items: + $ref: '#/components/schemas/SecretSnapshot' + description: Project secret snapshots + '400': + description: Bad Request + security: + - apiKeyAuth: [] + /api/v1/workspace/{workspaceId}/secret-snapshots/count: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/workspace/{workspaceId}/secret-snapshots/rollback: + post: + summary: >- + Roll back project secrets to those captured in a secret snapshot + version. + description: >- + Roll back project secrets to those captured in a secret snapshot + version. + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + description: ID of project + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + secrets: + type: array + items: + $ref: '#/components/schemas/Secret' + description: Secrets rolled back to + '400': + description: Bad Request + security: + - apiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + version: + type: integer + description: Version of secret snapshot to roll back to + /api/v1/workspace/{workspaceId}/logs: + get: + summary: Return project (audit) logs + description: Return project (audit) logs + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + description: ID of project + - name: userId + description: ID of project member + required: false + in: query + schema: + type: string + - name: offset + description: Number of logs to skip + required: false + in: query + schema: + type: string + - name: limit + description: Maximum number of logs to return + required: false + in: query + schema: + type: string + - name: sortBy + description: Order to sort the logs by + schema: + type: string + enum: + - oldest + - recent + required: false + in: query + - name: actionNames + description: Names of log actions (comma-separated) + required: false + in: query + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + logs: + type: array + items: + $ref: '#/components/schemas/Log' + description: Project logs + '400': + description: Bad Request + security: + - apiKeyAuth: [] + /api/v1/action/{actionId}: + get: + description: '' + parameters: + - name: actionId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/signup/email/signup: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + '403': + description: Forbidden + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + /api/v1/signup/email/verify: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + '403': + description: Forbidden + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + code: + example: any + /api/v1/signup/complete-account/signup: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + '403': + description: Forbidden + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + firstName: + example: any + lastName: + example: any + publicKey: + example: any + encryptedPrivateKey: + example: any + iv: + example: any + tag: + example: any + salt: + example: any + verifier: + example: any + organizationName: + example: any + /api/v1/signup/complete-account/invite: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + '403': + description: Forbidden + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + firstName: + example: any + lastName: + example: any + publicKey: + example: any + encryptedPrivateKey: + example: any + iv: + example: any + tag: + example: any + salt: + example: any + verifier: + example: any + /api/v1/auth/token: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/auth/login1: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + clientPublicKey: + example: any + /api/v1/auth/login2: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + clientProof: + example: any + /api/v1/auth/logout: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/auth/checkAuth: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + /api/v1/bot/{workspaceId}: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/bot/{botId}/active: + patch: + description: '' + parameters: + - name: botId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + isActive: + example: any + botKey: + example: any + /api/v1/user/: + get: + description: '' + parameters: [] + responses: + '200': + description: OK + /api/v1/user-action/: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + action: + example: any + get: + description: '' + parameters: + - name: action + in: query + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/organization/: + get: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + organizationName: + example: any + /api/v1/organization/{organizationId}: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/organization/{organizationId}/users: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/organization/{organizationId}/my-workspaces: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/organization/{organizationId}/name: + patch: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + example: any + /api/v1/organization/{organizationId}/incidentContactOrg: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + post: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + delete: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + /api/v1/organization/{organizationId}/customer-portal-session: + post: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/organization/{organizationId}/subscriptions: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/workspace/{workspaceId}/keys: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/workspace/{workspaceId}/users: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/workspace/: + get: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + workspaceName: + example: any + organizationId: + example: any + /api/v1/workspace/{workspaceId}: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + delete: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/workspace/{workspaceId}/name: + post: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + example: any + /api/v1/workspace/{workspaceId}/invite-signup: + post: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + /api/v1/workspace/{workspaceId}/integrations: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/workspace/{workspaceId}/authorizations: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/workspace/{workspaceId}/service-tokens: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/membership-org/membershipOrg/{membershipOrgId}/change-role: + post: + description: '' + parameters: + - name: membershipOrgId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/membership-org/{membershipOrgId}: + delete: + description: '' + parameters: + - name: membershipOrgId + in: path + required: true + schema: + type: string + responses: + '400': + description: Bad Request + /api/v1/membership/{workspaceId}/connect: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/membership/{membershipId}: + delete: + description: '' + parameters: + - name: membershipId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/membership/{membershipId}/change-role: + post: + description: '' + parameters: + - name: membershipId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + role: + example: any + /api/v1/key/{workspaceId}: + post: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + key: + example: any + /api/v1/key/{workspaceId}/latest: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/invite-org/signup: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + organizationId: + example: any + inviteeEmail: + example: any + /api/v1/invite-org/verify: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + code: + example: any + /api/v1/secret/{workspaceId}: + post: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + secrets: + example: any + keys: + example: any + environment: + example: any + channel: + example: any + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + - name: environment + in: query + schema: + type: string + - name: channel + in: query + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/secret/{workspaceId}/service-token: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + - name: environment + in: query + schema: + type: string + - name: channel + in: query + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/service-token/: + get: + description: '' + parameters: [] + responses: + '200': + description: OK + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + example: any + workspaceId: + example: any + environment: + example: any + expiresIn: + example: any + publicKey: + example: any + encryptedKey: + example: any + nonce: + example: any + /api/v1/password/srp1: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + clientPublicKey: + example: any + /api/v1/password/change-password: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + clientProof: + example: any + encryptedPrivateKey: + example: any + iv: + example: any + tag: + example: any + salt: + example: any + verifier: + example: any + /api/v1/password/email/password-reset: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + '403': + description: Forbidden + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + /api/v1/password/email/password-reset-verify: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + '403': + description: Forbidden + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + code: + example: any + /api/v1/password/backup-private-key: + get: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + clientProof: + example: any + encryptedPrivateKey: + example: any + iv: + example: any + tag: + example: any + salt: + example: any + verifier: + example: any + /api/v1/password/password-reset: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + encryptedPrivateKey: + example: any + iv: + example: any + tag: + example: any + salt: + example: any + verifier: + example: any + /api/v1/stripe/webhook: + post: + description: '' + parameters: + - name: stripe-signature + in: header + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/integration/{integrationId}: + patch: + description: '' + parameters: + - name: integrationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + app: + example: any + environment: + example: any + isActive: + example: any + target: + example: any + context: + example: any + siteId: + example: any + delete: + description: '' + parameters: + - name: integrationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/integration-auth/integration-options: + get: + description: '' + parameters: [] + responses: + '200': + description: OK + /api/v1/integration-auth/oauth-token: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + workspaceId: + example: any + code: + example: any + integration: + example: any + /api/v1/integration-auth/{integrationAuthId}/apps: + get: + description: '' + parameters: + - name: integrationAuthId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/integration-auth/{integrationAuthId}: + delete: + description: '' + parameters: + - name: integrationAuthId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v2/users/me: + get: + summary: Retrieve the current user on the request + description: Retrieve the current user on the request + parameters: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + user: + type: object + $ref: '#/components/schemas/CurrentUser' + description: Current user on request + '400': + description: Bad Request + security: + - apiKeyAuth: [] + /api/v2/workspace/{workspaceId}/secrets: + post: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + secrets: + example: any + keys: + example: any + environment: + example: any + channel: + example: any + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + - name: environment + in: query + schema: + type: string + - name: channel + in: query + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v2/workspace/{workspaceId}/encrypted-key: + get: + summary: Return encrypted project key + description: Return encrypted project key + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + description: ID of project + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectKey' + description: Encrypted project key for the given project + '400': + description: Bad Request + security: + - apiKeyAuth: [] + /api/v2/workspace/{workspaceId}/service-token-data: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v2/workspace/{workspaceId}/memberships: + get: + summary: Return project memberships + description: Return project memberships + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + description: ID of project + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + memberships: + type: array + items: + $ref: '#/components/schemas/Membership' + description: Memberships of project + '400': + description: Bad Request + security: + - apiKeyAuth: [] + /api/v2/workspace/{workspaceId}/memberships/{membershipId}: + delete: + summary: Delete project membership + description: Delete project membership + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + description: ID of project + - name: membershipId + in: path + required: true + schema: + type: string + description: ID of membership + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + membership: + $ref: '#/components/schemas/Membership' + description: Deleted membership + '400': + description: Bad Request + security: + - apiKeyAuth: [] + patch: + summary: Update project membership + description: Update project membership + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + description: ID of project + - name: membershipId + in: path + required: true + schema: + type: string + description: ID of membership + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + membership: + $ref: '#/components/schemas/Membership' + description: Updated membership + '400': + description: Bad Request + security: + - apiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + role: + type: string + description: Role of membership - either admin or member + /api/v2/secret/batch-create/workspace/{workspaceId}/environment/{environment}: + post: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + - name: environment + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + type: object + properties: + secrets: + example: any + /api/v2/secret/workspace/{workspaceId}/environment/{environment}: + post: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + - name: environment + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + type: object + properties: + secret: + example: any + /api/v2/secret/workspace/{workspaceId}: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + - name: environment + in: query + schema: + type: string + responses: + '200': + description: OK + /api/v2/secret/{secretId}: + get: + description: '' + parameters: + - name: secretId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + delete: + description: '' + parameters: + - name: secretId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v2/secret/batch/workspace/{workspaceId}/environment/{environmentName}: + delete: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + - name: environmentName + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + type: object + properties: + secretIds: + example: any + /api/v2/secret/batch-modify/workspace/{workspaceId}/environment/{environmentName}: + patch: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + - name: environmentName + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + type: object + properties: + secrets: + example: any + /api/v2/secret/workspace/{workspaceId}/environment/{environmentName}: + patch: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + - name: environmentName + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + type: object + properties: + secret: + example: any + /api/v2/secrets/: + post: + summary: Create new secret(s) + description: Create one or many secrets for a given project and environment. + parameters: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + secrets: + type: array + items: + $ref: '#/components/schemas/Secret' + description: >- + Newly-created secrets for the given project and + environment + security: + - apiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + workspaceId: + type: string + description: ID of project + environment: + type: string + description: Environment within project + secrets: + $ref: '#/components/schemas/CreateSecret' + description: Secret(s) to create - object or array of objects + get: + summary: Read secrets + description: Read secrets from a project and environment + parameters: + - name: workspaceId + description: ID of project + required: true + in: query + schema: + type: string + - name: environment + description: Environment within project + required: true + in: query + schema: + type: string + - name: content + in: query + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + secrets: + type: array + items: + $ref: '#/components/schemas/Secret' + description: Secrets for the given project and environment + security: + - apiKeyAuth: [] + patch: + summary: Update secret(s) + description: Update secret(s) + parameters: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + secrets: + type: array + items: + $ref: '#/components/schemas/Secret' + description: Updated secrets + security: + - apiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + secrets: + $ref: '#/components/schemas/UpdateSecret' + description: Secret(s) to update - object or array of objects + delete: + summary: Delete secret(s) + description: Delete one or many secrets by their ID(s) + parameters: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + secrets: + type: array + items: + $ref: '#/components/schemas/Secret' + description: Deleted secrets + security: + - apiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + secretIds: + type: string + description: ID(s) of secrets - string or array of strings + /api/v2/service-token/: + get: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + example: any + workspaceId: + example: any + environment: + example: any + encryptedKey: + example: any + iv: + example: any + tag: + example: any + expiresIn: + example: any + /api/v2/service-token/{serviceTokenDataId}: + delete: + description: '' + parameters: + - name: serviceTokenDataId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v2/api-key-data/: + get: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + example: any + expiresIn: + example: any + /api/v2/api-key-data/{apiKeyDataId}: + delete: + description: '' + parameters: + - name: apiKeyDataId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/status: + get: + description: '' + parameters: [] + responses: + '200': + description: OK +components: + schemas: + CurrentUser: + type: object + properties: + _id: + type: string + example: '' + email: + type: string + example: '' + firstName: + type: string + example: '' + lastName: + type: string + example: '' + publicKey: + type: string + example: '' + encryptedPrivateKey: + type: string + example: '' + iv: + type: string + example: '' + tag: + type: string + example: '' + updatedAt: + type: string + example: '' + createdAt: + type: string + example: '' + Membership: + type: object + properties: + user: + type: object + properties: + _id: + type: string + example: '' + email: + type: string + example: '' + firstName: + type: string + example: '' + lastName: + type: string + example: '' + publicKey: + type: string + example: '' + updatedAt: + type: string + example: '' + createdAt: + type: string + example: '' + workspace: + type: string + example: '' + role: + type: string + example: admin + ProjectKey: + type: object + properties: + encryptedkey: + type: string + example: '' + nonce: + type: string + example: '' + sender: + type: object + properties: + publicKey: + type: string + example: '' + receiver: + type: string + example: '' + workspace: + type: string + example: '' + CreateSecret: + type: object + properties: + type: + type: string + example: shared + secretKeyCiphertext: + type: string + example: '' + secretKeyIV: + type: string + example: '' + secretKeyTag: + type: string + example: '' + secretValueCiphertext: + type: string + example: '' + secretValueIV: + type: string + example: '' + secretValueTag: + type: string + example: '' + secretCommentCiphertext: + type: string + example: '' + secretCommentIV: + type: string + example: '' + secretCommentTag: + type: string + example: '' + UpdateSecret: + type: object + properties: + id: + type: string + example: '' + secretKeyCiphertext: + type: string + example: '' + secretKeyIV: + type: string + example: '' + secretKeyTag: + type: string + example: '' + secretValueCiphertext: + type: string + example: '' + secretValueIV: + type: string + example: '' + secretValueTag: + type: string + example: '' + secretCommentCiphertext: + type: string + example: '' + secretCommentIV: + type: string + example: '' + secretCommentTag: + type: string + example: '' + Secret: + type: object + properties: + _id: + type: string + example: '' + version: + type: number + example: 1 + workspace: + type: string + example: '' + type: + type: string + example: shared + user: {} + secretKeyCiphertext: + type: string + example: '' + secretKeyIV: + type: string + example: '' + secretKeyTag: + type: string + example: '' + secretValueCiphertext: + type: string + example: '' + secretValueIV: + type: string + example: '' + secretValueTag: + type: string + example: '' + secretCommentCiphertext: + type: string + example: '' + secretCommentIV: + type: string + example: '' + secretCommentTag: + type: string + example: '' + updatedAt: + type: string + example: '' + createdAt: + type: string + example: '' + Log: + type: object + properties: + _id: + type: string + example: '' + user: + type: object + properties: + _id: + type: string + example: '' + email: + type: string + example: '' + firstName: + type: string + example: '' + lastName: + type: string + example: '' + workspace: + type: string + example: '' + actionNames: + type: array + example: + - addSecrets + items: + type: string + actions: + type: array + items: + type: object + properties: + name: + type: string + example: addSecrets + user: + type: string + example: '' + workspace: + type: string + example: '' + payload: + type: array + items: + type: object + properties: + oldSecretVersion: + type: string + example: '' + newSecretVersion: + type: string + example: '' + channel: + type: string + example: cli + ipAddress: + type: string + example: 192.168.0.1 + updatedAt: + type: string + example: '' + createdAt: + type: string + example: '' + SecretSnapshot: + type: object + properties: + workspace: + type: string + example: '' + version: + type: number + example: 1 + secretVersions: + type: array + items: + type: object + properties: + _id: + type: string + example: '' + SecretVersion: + type: object + properties: + _id: + type: string + example: '' + secret: + type: string + example: '' + version: + type: number + example: 1 + workspace: + type: string + example: '' + type: + type: string + example: '' + user: + type: string + example: '' + environment: + type: string + example: '' + isDeleted: + type: string + example: '' + secretKeyCiphertext: + type: string + example: '' + secretKeyIV: + type: string + example: '' + secretKeyTag: + type: string + example: '' + secretValueCiphertext: + type: string + example: '' + secretValueIV: + type: string + example: '' + secretValueTag: + type: string + example: '' + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: >- + This security definition uses the HTTP 'bearer' scheme, which allows the + client to authenticate using a JSON Web Token (JWT) that is passed in + the Authorization header of the request. + apiKeyAuth: + type: apiKey + in: header + name: X-API-Key + description: >- + This security definition uses an API key, which is passed in the header + of the request as the value of the "X-API-Key" header. The client must + provide a valid key in order to access the API. diff --git a/frontend/components/utilities/attemptLogin.ts b/frontend/components/utilities/attemptLogin.ts index f1aa03ffb..1a960af10 100644 --- a/frontend/components/utilities/attemptLogin.ts +++ b/frontend/components/utilities/attemptLogin.ts @@ -61,7 +61,7 @@ const attemptLogin = async ( // if everything works, go the main dashboard page. const { token, publicKey, encryptedPrivateKey, iv, tag } = await login2(email, clientProof); - + SecurityClient.setToken(token); const privateKey = Aes256Gcm.decrypt({