From 9d4ea2dcda6e6c399e1a743de974c570ec2cc386 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 13 Jan 2023 15:04:46 +0700 Subject: [PATCH] Continue api-reference docs --- backend/package-lock.json | 9 +- backend/package.json | 3 +- backend/{api-documentation.json => spec.json} | 651 ++++++++++++++++-- backend/src/app.ts | 6 +- backend/src/controllers/v1/authController.ts | 5 +- backend/src/controllers/v2/index.ts | 2 + .../src/controllers/v2/secretsController.ts | 171 +++++ backend/src/controllers/v2/usersController.ts | 44 ++ .../src/controllers/v2/workspaceController.ts | 28 + backend/src/routes/v2/index.ts | 2 + backend/src/routes/v2/users.ts | 16 + backend/swagger.ts | 22 - backend/swagger/index.ts | 116 ++++ backend/swagger/schemas/index.ts | 6 + backend/swagger/schemas/secretSchema.ts | 11 + docs/api-reference/endpoints/secrets/read.mdx | 2 +- docs/api-reference/endpoints/users/me.mdx | 4 + .../endpoints/workspaces/workspace-key.mdx | 4 + .../api-reference/overview/authentication.mdx | 8 + .../overview/examples/create-secrets.mdx | 64 ++ .../overview/examples/retrieve-secrets.mdx | 10 + .../overview/examples/update-secrets.mdx | 10 + docs/api-reference/overview/introduction.mdx | 16 + docs/api-reference/overview/usage.mdx | 18 + docs/mint.json | 32 +- docs/spec.yaml | 328 ++++++++- 26 files changed, 1466 insertions(+), 122 deletions(-) rename backend/{api-documentation.json => spec.json} (79%) create mode 100644 backend/src/controllers/v2/usersController.ts create mode 100644 backend/src/routes/v2/users.ts delete mode 100644 backend/swagger.ts create mode 100644 backend/swagger/index.ts create mode 100644 backend/swagger/schemas/index.ts create mode 100644 backend/swagger/schemas/secretSchema.ts create mode 100644 docs/api-reference/endpoints/users/me.mdx create mode 100644 docs/api-reference/endpoints/workspaces/workspace-key.mdx create mode 100644 docs/api-reference/overview/examples/create-secrets.mdx create mode 100644 docs/api-reference/overview/examples/retrieve-secrets.mdx create mode 100644 docs/api-reference/overview/examples/update-secrets.mdx create mode 100644 docs/api-reference/overview/usage.mdx diff --git a/backend/package-lock.json b/backend/package-lock.json index b51aa7cf09..38c1126ccf 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 08f5a815cb..b9359f6800 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 79% rename from backend/api-documentation.json rename to backend/spec.json index 79cf0eb3b1..1883652905 100644 --- a/backend/api-documentation.json +++ b/backend/spec.json @@ -5,6 +5,16 @@ "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": { @@ -43,6 +53,43 @@ } } }, + "/api/v1/secret/{secretId}/secret-versions/rollback": { + "post": { + "description": "", + "parameters": [ + { + "name": "secretId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "version": { + "example": "any" + } + } + } + } + } + } + } + }, "/api/v1/secret-snapshot/{secretSnapshotId}": { "get": { "description": "", @@ -126,6 +173,43 @@ } } }, + "/api/v1/workspace/{workspaceId}/secret-snapshots/rollback": { + "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": { + "version": { + "example": "any" + } + } + } + } + } + } + } + }, "/api/v1/workspace/{workspaceId}/logs": { "get": { "description": "", @@ -476,7 +560,11 @@ "post": { "description": "", "parameters": [], - "responses": {} + "responses": { + "200": { + "description": "OK" + } + } } }, "/api/v1/bot/{workspaceId}": { @@ -1997,6 +2085,35 @@ } } }, + "/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", + "$ref": "#/components/schemas/CurrentUser", + "description": "Current user on request" + } + } + } + }, + "400": { + "description": "Bad Request" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + } + }, "/api/v2/workspace/{workspaceId}/secrets": { "post": { "description": "", @@ -2080,7 +2197,8 @@ }, "/api/v2/workspace/{workspaceId}/encrypted-key": { "get": { - "description": "", + "summary": "Return encrypted project key", + "description": "Return encrypted project key", "parameters": [ { "name": "workspaceId", @@ -2088,17 +2206,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 +2259,7 @@ } } }, - "/api/v2/secret/batch-create/workspace/{workspaceId}/environment/{environmentName}": { + "/api/v2/secret/batch-create/workspace/{workspaceId}/environment/{environment}": { "post": { "description": "", "parameters": [ @@ -2137,7 +2272,7 @@ } }, { - "name": "environmentName", + "name": "environment", "in": "path", "required": true, "schema": { @@ -2166,7 +2301,7 @@ } } }, - "/api/v2/secret/workspace/{workspaceId}/environment/{environmentName}": { + "/api/v2/secret/workspace/{workspaceId}/environment/{environment}": { "post": { "description": "", "parameters": [ @@ -2179,47 +2314,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 +2492,230 @@ } } }, + "/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": "array", + "items": { + "$ref": "#/components/schemas/Secret" + }, + "description": "Array of 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": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Secret" + }, + "description": "Array of 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" + } + } + } + } + } + } + }, + "patch": { + "summary": "Update secret(s)", + "description": "Update secret(s)", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Secret" + }, + "description": "Array of newly-updated secrets for the given project and environment" + } + } + } + } + }, + "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": "array", + "items": { + "$ref": "#/components/schemas/Secret" + }, + "description": "Array of 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 +2876,243 @@ } }, "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": "" + }, + "updatedAt": { + "type": "string", + "example": "" + }, + "createdAt": { + "type": "string", + "example": "" + } + } + }, + "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": "" + } + } + } + }, "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 aa7ac9b28e..8cefd31ac2 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, @@ -103,6 +104,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', v2WorkspaceRouter); // TODO: turn into plural route app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise app.use('/api/v2/secrets', v2SecretsRouter); diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index defd03d8a0..882db688f6 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 1651c09ee5..bc68485901 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'; @@ -5,6 +6,7 @@ import * as secretController from './secretController'; import * as secretsController from './secretsController'; export { + usersController, workspaceController, serviceTokenDataController, apiKeyDataController, diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 69cacd75c6..31ad0d94c7 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -21,6 +21,53 @@ 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": "array", + "items": { + $ref: "#/components/schemas/Secret" + }, + "description": "Array of 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; @@ -150,6 +197,49 @@ 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.requestBody = { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "description": "ID of project" + }, + "environment": { + "type": "string", + "description": "Environment within project" + } + } + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "array", + "items": { + $ref: "#/components/schemas/Secret" + }, + "description": "Array of 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 @@ -217,6 +307,48 @@ export const getSecrets = async (req: Request, res: Response) => { * @param res */ export const updateSecrets = async (req: Request, res: Response) => { + + // TODO: fix update secret schema + + /* + #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": "array", + "items": { + $ref: "#/components/schemas/Secret" + }, + "description": "Array of newly-updated secrets for the given project and environment" + } + } + } + } + */ const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; // TODO: move type @@ -384,6 +516,45 @@ 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": "array", + "items": { + $ref: "#/components/schemas/Secret" + }, + "description": "Array of 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 0000000000..faac8f646b --- /dev/null +++ b/backend/src/controllers/v2/usersController.ts @@ -0,0 +1,44 @@ +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", + $ref: "#/components/schemas/CurrentUser", + "description": "Current user on request" + } + } + } + } + */ + let user; + try { + user = await User + .findById(req.user._id) + .select('+publicKey +encryptedPrivateKey'); + } 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 0dbdfa0769..6acec644d9 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -169,6 +169,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; diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts index 8bea42620b..a30c72642a 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'; @@ -5,6 +6,7 @@ import serviceTokenData from './serviceTokenData'; import apiKeyData from './apiKeyData'; 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 0000000000..dba107b154 --- /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/swagger.ts b/backend/swagger.ts deleted file mode 100644 index 0505bb813a..0000000000 --- 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 0000000000..2b8bc4f623 --- /dev/null +++ b/backend/swagger/index.ts @@ -0,0 +1,116 @@ +/* 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: '', + updatedAt: '', + createdAt: '' + }, + 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: '' + } + } + }; + + 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 0000000000..b0b427fce8 --- /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 0000000000..85c6ce0179 --- /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 4305f192c5..c88fb0ec78 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/users/me.mdx b/docs/api-reference/endpoints/users/me.mdx new file mode 100644 index 0000000000..9273f0ca63 --- /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/workspace-key.mdx b/docs/api-reference/endpoints/workspaces/workspace-key.mdx new file mode 100644 index 0000000000..2658676a30 --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/workspace-key.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Project 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 27a2dc1345..8cc2182532 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 0000000000..94f8300941 --- /dev/null +++ b/docs/api-reference/overview/examples/create-secrets.mdx @@ -0,0 +1,64 @@ +--- +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 project key for the project. +4. Decrypt the project key with your private key. +5. Encrypt your secrets with the project key. +6. Send (encrypted) secrets to the Infical API + +## Example + +```js +const axios = require("axios"); +const aes = require("aes-256-gcm"); +const nacl = require("tweetnacl"); +nacl.util = require("tweetnacl-util"); + +const WORKSPACE_KEY = "3a7a243eb62078c13f09203e75e8cb32"; + +const secretKey = "SOME_KEY"; +const secretValue = "SOME_VALUE"; + +// encrypt key of secret +const { + ciphertext: secretKeyCiphertext, + iv: secretKeyIV, + tag: secretKeyTag, +} = aes.encrypt(secretKey, WORKSPACE_KEY); + +// encrypt value of secret +const { + ciphertext: secretValueCiphertext, + iv: secretValueIV, + tag: secretValueTag, +} = aes.encrypt(secretKey, WORKSPACE_KEY); + +// construct request body +const secret = { + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, +}; +``` + + + 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 in every major language. + + 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 0000000000..2a314e65dd --- /dev/null +++ b/docs/api-reference/overview/examples/retrieve-secrets.mdx @@ -0,0 +1,10 @@ +--- +title: "Retrieve secrets" +--- + +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. Get secrets for a project and environment. +6. Decrypt the secrets in your project. 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 0000000000..b302710a37 --- /dev/null +++ b/docs/api-reference/overview/examples/update-secrets.mdx @@ -0,0 +1,10 @@ +--- +title: "Update secrets" +--- + +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 secrets with the project key. +6. Send (encrypted) updated secrets to the Infical API diff --git a/docs/api-reference/overview/introduction.mdx b/docs/api-reference/overview/introduction.mdx index 9632e3788d..f93ba91fe7 100644 --- a/docs/api-reference/overview/introduction.mdx +++ b/docs/api-reference/overview/introduction.mdx @@ -1,3 +1,19 @@ --- title: "Introduction" --- + +Infisical's REST API provides users an alternative way to programmatically access and manage +secrets via HTTP 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. diff --git a/docs/api-reference/overview/usage.mdx b/docs/api-reference/overview/usage.mdx new file mode 100644 index 0000000000..9f23080c7e --- /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 363b27a3e0..39a3726d94 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" } ], @@ -134,12 +144,32 @@ "group": "Overview", "pages": [ "api-reference/overview/introduction", - "api-reference/overview/authentication" + "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" + ] + } ] }, { "group": "Endpoints", "pages": [ + { + "group": "Users", + "pages": [ + "api-reference/endpoints/users/me" + ] + }, + { + "group": "Projects", + "pages": [ + "api-reference/endpoints/workspaces/workspace-key" + ] + }, { "group": "Secrets", "pages": [ diff --git a/docs/spec.yaml b/docs/spec.yaml index a0271395b1..dd62f167be 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -4,7 +4,10 @@ info: 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: @@ -1270,6 +1273,24 @@ paths: 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 + $ref: '#/components/schemas/CurrentUser' + description: Current user on request + '400': + description: Bad Request + security: + - apiKeyAuth: [] /api/v2/workspace/{workspaceId}/secrets: post: description: '' @@ -1321,18 +1342,29 @@ paths: description: Bad Request /api/v2/workspace/{workspaceId}/encrypted-key: get: - description: '' + 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: '' @@ -1513,57 +1545,122 @@ paths: example: any /api/v2/secrets/: post: - description: '' + 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: array + items: + $ref: '#/components/schemas/Secret' + description: >- + Array of newly-created secrets for the given project and + environment + security: + - apiKeyAuth: [] requestBody: + required: true content: application/json: schema: type: object properties: - secrets: - example: any workspaceId: - example: any + type: string + description: ID of project environment: - example: any + type: string + description: Environment within project + secrets: + $ref: '#/components/schemas/CreateSecret' + description: Secret(s) to create - object or array of objects get: - description: '' - parameters: - - name: workspaceId - in: query - schema: - type: string - - name: environment - in: query - schema: - type: string - responses: - '200': - description: OK - patch: - description: '' + summary: Read secrets + description: Read secrets from a project and environment parameters: [] responses: '200': description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: Array of 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 + patch: + summary: Update secret(s) + description: Update secret(s) + parameters: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: >- + Array of newly-updated secrets for the given project and + environment + security: + - apiKeyAuth: [] + requestBody: + required: true content: application/json: schema: type: object properties: secrets: - example: any + $ref: '#/components/schemas/UpdateSecret' + description: Secret(s) to update - object or array of objects delete: - description: '' + summary: Delete secret(s) + description: Delete one or many secrets by their ID(s) parameters: [] responses: '200': description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Secret' + description: Array of 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: '' @@ -1665,26 +1762,183 @@ paths: description: OK components: schemas: - secret: + 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: '' + updatedAt: + type: string + example: '' + createdAt: + type: string + example: '' + 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: object - properties: - type: object - properties: - test: - type: object - properties: - type: - type: string - example: integer - description: - type: string - example: '123' + 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: '' 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.