diff --git a/backend/spec.json b/backend/spec.json index 0b0e275b65..f0af8d9035 100644 --- a/backend/spec.json +++ b/backend/spec.json @@ -492,119 +492,6 @@ } } }, - "/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": "", @@ -653,7 +540,15 @@ "/api/v1/auth/login2": { "post": { "description": "", - "parameters": [], + "parameters": [ + { + "name": "user-agent", + "in": "header", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK" @@ -684,7 +579,15 @@ "/api/v1/auth/logout": { "post": { "description": "", - "parameters": [], + "parameters": [ + { + "name": "user-agent", + "in": "header", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK" @@ -1113,6 +1016,26 @@ } } }, + "/api/v1/organization/{organizationId}/workspace-memberships": { + "get": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/v1/workspace/{workspaceId}/keys": { "get": { "description": "", @@ -1512,6 +1435,40 @@ } } }, + "/api/v1/membership/{membershipId}/deny-permissions": { + "post": { + "description": "", + "parameters": [ + { + "name": "membershipId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "permissions": { + "example": "any" + } + } + } + } + } + } + } + }, "/api/v1/key/{workspaceId}": { "post": { "description": "", @@ -1856,13 +1813,22 @@ "clientProof": { "example": "any" }, + "protectedKey": { + "example": "any" + }, + "protectedKeyIV": { + "example": "any" + }, + "protectedKeyTag": { + "example": "any" + }, "encryptedPrivateKey": { "example": "any" }, - "iv": { + "encryptedPrivateKeyIV": { "example": "any" }, - "tag": { + "encryptedPrivateKeyTag": { "example": "any" }, "salt": { @@ -2016,13 +1982,22 @@ "schema": { "type": "object", "properties": { + "protectedKey": { + "example": "any" + }, + "protectedKeyIV": { + "example": "any" + }, + "protectedKeyTag": { + "example": "any" + }, "encryptedPrivateKey": { "example": "any" }, - "iv": { + "encryptedPrivateKeyIV": { "example": "any" }, - "tag": { + "encryptedPrivateKeyTag": { "example": "any" }, "salt": { @@ -2060,6 +2035,58 @@ } } }, + "/api/v1/integration/": { + "post": { + "description": "", + "parameters": [], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "integrationAuthId": { + "example": "any" + }, + "app": { + "example": "any" + }, + "appId": { + "example": "any" + }, + "isActive": { + "example": "any" + }, + "sourceEnvironment": { + "example": "any" + }, + "targetEnvironment": { + "example": "any" + }, + "owner": { + "example": "any" + }, + "path": { + "example": "any" + }, + "region": { + "example": "any" + } + } + } + } + } + } + } + }, "/api/v1/integration/{integrationId}": { "patch": { "description": "", @@ -2087,22 +2114,22 @@ "schema": { "type": "object", "properties": { - "app": { - "example": "any" - }, "environment": { "example": "any" }, "isActive": { "example": "any" }, - "target": { + "app": { + "example": "any" + }, + "appId": { "example": "any" }, - "context": { + "targetEnvironment": { "example": "any" }, - "siteId": { + "owner": { "example": "any" } } @@ -2144,15 +2171,59 @@ } } }, - "/api/v1/integration-auth/oauth-token": { - "post": { + "/api/v1/integration-auth/{integrationAuthId}": { + "get": { "description": "", - "parameters": [], - "responses": { - "200": { - "description": "OK" - }, - "400": { + "parameters": [ + { + "name": "integrationAuthId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + }, + "delete": { + "description": "", + "parameters": [ + { + "name": "integrationAuthId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/v1/integration-auth/oauth-token": { + "post": { + "description": "", + "parameters": [], + "responses": { + "200": { + "description": "OK" + }, + "400": { "description": "Bad Request" } }, @@ -2178,6 +2249,43 @@ } } }, + "/api/v1/integration-auth/access-token": { + "post": { + "description": "", + "parameters": [], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "workspaceId": { + "example": "any" + }, + "accessId": { + "example": "any" + }, + "accessToken": { + "example": "any" + }, + "integration": { + "example": "any" + } + } + } + } + } + } + } + }, "/api/v1/integration-auth/{integrationAuthId}/apps": { "get": { "description": "", @@ -2201,14 +2309,175 @@ } } }, - "/api/v1/integration-auth/{integrationAuthId}": { - "delete": { + "/api/v2/signup/complete-account/signup": { + "post": { + "description": "", + "parameters": [], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + }, + "403": { + "description": "Forbidden" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "example": "any" + }, + "firstName": { + "example": "any" + }, + "lastName": { + "example": "any" + }, + "protectedKey": { + "example": "any" + }, + "protectedKeyIV": { + "example": "any" + }, + "protectedKeyTag": { + "example": "any" + }, + "publicKey": { + "example": "any" + }, + "encryptedPrivateKey": { + "example": "any" + }, + "encryptedPrivateKeyIV": { + "example": "any" + }, + "encryptedPrivateKeyTag": { + "example": "any" + }, + "salt": { + "example": "any" + }, + "verifier": { + "example": "any" + }, + "organizationName": { + "example": "any" + } + } + } + } + } + } + } + }, + "/api/v2/signup/complete-account/invite": { + "post": { + "description": "", + "parameters": [], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + }, + "403": { + "description": "Forbidden" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "example": "any" + }, + "firstName": { + "example": "any" + }, + "lastName": { + "example": "any" + }, + "protectedKey": { + "example": "any" + }, + "protectedKeyIV": { + "example": "any" + }, + "protectedKeyTag": { + "example": "any" + }, + "publicKey": { + "example": "any" + }, + "encryptedPrivateKey": { + "example": "any" + }, + "encryptedPrivateKeyIV": { + "example": "any" + }, + "encryptedPrivateKeyTag": { + "example": "any" + }, + "salt": { + "example": "any" + }, + "verifier": { + "example": "any" + } + } + } + } + } + } + } + }, + "/api/v2/auth/login1": { + "post": { + "description": "", + "parameters": [], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "example": "any" + }, + "clientPublicKey": { + "example": "any" + } + } + } + } + } + } + } + }, + "/api/v2/auth/login2": { + "post": { "description": "", "parameters": [ { - "name": "integrationAuthId", - "in": "path", - "required": true, + "name": "user-agent", + "in": "header", "schema": { "type": "string" } @@ -2221,6 +2490,87 @@ "400": { "description": "Bad Request" } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "example": "any" + }, + "clientProof": { + "example": "any" + } + } + } + } + } + } + } + }, + "/api/v2/auth/mfa/send": { + "post": { + "description": "", + "parameters": [], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "example": "any" + } + } + } + } + } + } + } + }, + "/api/v2/auth/mfa/verify": { + "post": { + "description": "", + "parameters": [ + { + "name": "user-agent", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "example": "any" + }, + "mfaToken": { + "example": "any" + } + } + } + } + } } } }, @@ -2258,6 +2608,34 @@ ] } }, + "/api/v2/users/me/mfa": { + "patch": { + "description": "", + "parameters": [], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "isMfaEnabled": { + "example": "any" + } + } + } + } + } + } + } + }, "/api/v2/users/me/organizations": { "get": { "summary": "Return organizations that current user is part of", @@ -2543,7 +2921,48 @@ } } }, - "put": { + "put": { + "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": { + "environmentName": { + "example": "any" + }, + "environmentSlug": { + "example": "any" + }, + "oldEnvironmentSlug": { + "example": "any" + } + } + } + } + } + } + }, + "delete": { "description": "", "parameters": [ { @@ -2569,14 +2988,8 @@ "schema": { "type": "object", "properties": { - "environmentName": { - "example": "any" - }, "environmentSlug": { "example": "any" - }, - "oldEnvironmentSlug": { - "example": "any" } } } @@ -2584,7 +2997,45 @@ } } }, - "delete": { + "get": { + "description": "", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v2/workspace/{workspaceId}/tags": { + "get": { + "description": "", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { "description": "", "parameters": [ { @@ -2599,9 +3050,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } }, "requestBody": { @@ -2610,7 +3058,10 @@ "schema": { "type": "object", "properties": { - "environmentSlug": { + "name": { + "example": "any" + }, + "slug": { "example": "any" } } @@ -2620,6 +3071,26 @@ } } }, + "/api/v2/workspace/tags/{tagId}": { + "delete": { + "description": "", + "parameters": [ + { + "name": "tagId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/v2/workspace/{workspaceId}/secrets": { "post": { "description": "", @@ -2929,6 +3400,43 @@ ] } }, + "/api/v2/workspace/{workspaceId}/auto-capitalization": { + "patch": { + "description": "", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "autoCapitalization": { + "example": "any" + } + } + } + } + } + } + } + }, "/api/v2/secret/batch-create/workspace/{workspaceId}/environment/{environment}": { "post": { "description": "", @@ -3204,11 +3712,58 @@ } } }, + "/api/v2/secrets/batch": { + "post": { + "description": "", + "parameters": [ + { + "name": "user-agent", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "workspaceId": { + "example": "any" + }, + "environment": { + "example": "any" + }, + "requests": { + "example": "any" + } + } + } + } + } + } + } + }, "/api/v2/secrets/": { "post": { "summary": "Create new secret(s)", "description": "Create one or many secrets for a given project and environment.", - "parameters": [], + "parameters": [ + { + "name": "user-agent", + "in": "header", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK", @@ -3282,6 +3837,13 @@ "type": "string" } }, + { + "name": "user-agent", + "in": "header", + "schema": { + "type": "string" + } + }, { "name": "content", "in": "query", @@ -3367,7 +3929,15 @@ "delete": { "summary": "Delete secret(s)", "description": "Delete one or many secrets by their ID(s)", - "parameters": [], + "parameters": [ + { + "name": "user-agent", + "in": "header", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK", @@ -3414,16 +3984,33 @@ }, "/api/v2/service-token/": { "get": { - "description": "", + "summary": "Return Infisical Token data", + "description": "Return Infisical Token data", "parameters": [], "responses": { "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "serviceTokenData": { + "type": "object", + "$ref": "#/components/schemas/ServiceTokenData", + "description": "Details of service token" + } + } + } + } + } } - } + }, + "security": [ + { + "bearerAuth": [] + } + ] }, "post": { "description": "", @@ -3462,6 +4049,9 @@ }, "expiresIn": { "example": "any" + }, + "permissions": { + "example": "any" } } } @@ -4126,6 +4716,68 @@ "example": "" } } + }, + "ServiceTokenData": { + "type": "object", + "properties": { + "_id": { + "type": "string", + "example": "" + }, + "name": { + "type": "string", + "example": "" + }, + "workspace": { + "type": "string", + "example": "" + }, + "environment": { + "type": "string", + "example": "" + }, + "user": { + "type": "object", + "properties": { + "_id": { + "type": "string", + "example": "" + }, + "firstName": { + "type": "string", + "example": "" + }, + "lastName": { + "type": "string", + "example": "" + } + } + }, + "expiresAt": { + "type": "string", + "example": "2023-01-13T14:16:12.210Z" + }, + "encryptedKey": { + "type": "string", + "example": "" + }, + "iv": { + "type": "string", + "example": "" + }, + "tag": { + "type": "string", + "example": "" + }, + "updatedAt": { + "type": "string", + "example": "2023-01-13T14:16:12.210Z" + }, + "createdAt": { + "type": "string", + "example": "2023-01-13T14:16:12.210Z" + } + } } }, "securitySchemes": { diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 29939a1cd6..486bf017e9 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -953,6 +953,7 @@ export const deleteSecrets = async (req: Request, res: Response) => { } } */ + const channel = getChannelFromUserAgent(req.headers['user-agent']) const toDelete = req.secrets.map((s: any) => s._id); diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index 780bae9a49..cabedabeab 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -17,7 +17,35 @@ import { ABILITY_READ } from '../../variables/organization'; * @param res * @returns */ -export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).json(req.serviceTokenData); +export const getServiceTokenData = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Return Infisical Token data' + #swagger.description = 'Return Infisical Token data' + + #swagger.security = [{ + "bearerAuth": [] + }] + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "serviceTokenData": { + "type": "object", + $ref: "#/components/schemas/ServiceTokenData", + "description": "Details of service token" + } + } + } + } + } + } + */ + + return res.status(200).json(req.serviceTokenData); +} /** * Create new service token data for workspace with id [workspaceId] and @@ -28,6 +56,7 @@ export const getServiceTokenData = async (req: Request, res: Response) => res.st */ export const createServiceTokenData = async (req: Request, res: Response) => { let serviceToken, serviceTokenData; + try { const { name, @@ -36,7 +65,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => { encryptedKey, iv, tag, - expiresIn + expiresIn, + permissions } = req.body; const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ) @@ -59,7 +89,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => { secretHash, encryptedKey, iv, - tag + tag, + permissions }).save(); // return service token data without sensitive data diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index f7f88ec0be..102a7ac078 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -2,6 +2,7 @@ import jwt from 'jsonwebtoken'; import * as Sentry from '@sentry/node'; import bcrypt from 'bcrypt'; import { + IUser, User, ServiceTokenData, APIKeyData @@ -148,7 +149,10 @@ const getAuthSTDPayload = async ({ serviceTokenData = await ServiceTokenData .findById(TOKEN_IDENTIFIER) - .select('+encryptedKey +iv +tag').populate('user'); + .select('+encryptedKey +iv +tag') + .populate<{user: IUser}>('user'); + + if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); } catch (err) { throw UnauthorizedRequestError({ diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 24ef92f06c..f4921398a5 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -1,12 +1,14 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; -import { User, ServiceTokenData } from '../models'; import { validateAuthMode, getAuthUserPayload, getAuthSTDPayload, getAuthAPIKeyPayload } from '../helpers/auth'; +import { + UnauthorizedRequestError +} from '../utils/errors'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -25,9 +27,11 @@ declare module 'jsonwebtoken' { * @returns */ const requireAuth = ({ - acceptedAuthModes = ['jwt'] + acceptedAuthModes = ['jwt'], + requiredServiceTokenPermissions = [] }: { acceptedAuthModes: string[]; + requiredServiceTokenPermissions?: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { // validate auth token against accepted auth modes [acceptedAuthModes] @@ -38,11 +42,22 @@ const requireAuth = ({ }); // attach auth payloads + let serviceTokenData: any; switch (authTokenType) { case 'serviceToken': - req.serviceTokenData = await getAuthSTDPayload({ + serviceTokenData = await getAuthSTDPayload({ authTokenValue }); + + requiredServiceTokenPermissions.forEach((requiredServiceTokenPermission) => { + if (!serviceTokenData.permissions.includes(requiredServiceTokenPermission)) { + return next(UnauthorizedRequestError({ message: 'Failed to authorize service token for endpoint' })); + } + }); + + req.serviceTokenData = serviceTokenData; + req.user = serviceTokenData?.user; + break; case 'apiKey': req.user = await getAuthAPIKeyPayload({ diff --git a/backend/src/middleware/requireSecretsAuth.ts b/backend/src/middleware/requireSecretsAuth.ts index c8b89a74c7..2a6c360562 100644 --- a/backend/src/middleware/requireSecretsAuth.ts +++ b/backend/src/middleware/requireSecretsAuth.ts @@ -23,7 +23,7 @@ const requireSecretsAuth = ({ // case: validate 1 secret secrets = await validateSecrets({ userId: req.user._id.toString(), - secretIds: req.body.secrets.id + secretIds: [req.body.secrets.id] }); } else if (Array.isArray(req.body.secretIds)) { secrets = await validateSecrets({ diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index 9b710cc83e..3d1259a399 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -21,7 +21,7 @@ const requireWorkspaceAuth = ({ return async (req: Request, res: Response, next: NextFunction) => { try { const { workspaceId } = req[location]; - + if (req.user) { // case: jwt auth const membership = await validateMembership({ @@ -32,11 +32,11 @@ const requireWorkspaceAuth = ({ req.membership = membership; } - + if ( req.serviceTokenData && req.serviceTokenData.workspace !== workspaceId - && req.serviceTokenData.environment !== req.query.environment + && req.serviceTokenData.environment !== req.body.environment ) { next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'})) } diff --git a/backend/src/models/serviceTokenData.ts b/backend/src/models/serviceTokenData.ts index 612d07bf63..2467b0b828 100644 --- a/backend/src/models/serviceTokenData.ts +++ b/backend/src/models/serviceTokenData.ts @@ -3,13 +3,14 @@ import { Schema, model, Types } from 'mongoose'; export interface IServiceTokenData { name: string; workspace: Types.ObjectId; - environment: string; // TODO: adapt to upcoming environment id + environment: string; user: Types.ObjectId; expiresAt: Date; secretHash: string; encryptedKey: string; iv: string; tag: string; + permissions: string[]; } const serviceTokenDataSchema = new Schema( @@ -51,6 +52,11 @@ const serviceTokenDataSchema = new Schema( tag: { type: String, select: false + }, + permissions: { + type: [String], + enum: ['read', 'write'], + default: ['read'] } }, { diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index 51629b32d4..983a8e54f1 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -22,7 +22,8 @@ import { router.post( '/batch', requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], + requiredServiceTokenPermissions: ['read', 'write'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -99,7 +100,8 @@ router.post( }), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], + requiredServiceTokenPermissions: ['write'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -115,7 +117,8 @@ router.get( query('tagSlugs'), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'] + acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], + requiredServiceTokenPermissions: ['read'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -154,7 +157,8 @@ router.patch( }), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], + requiredServiceTokenPermissions: ['write'] }), requireSecretsAuth({ acceptedRoles: [ADMIN, MEMBER] @@ -182,7 +186,8 @@ router.delete( .isEmpty(), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], + requiredServiceTokenPermissions: ['write'] }), requireSecretsAuth({ acceptedRoles: [ADMIN, MEMBER] @@ -192,5 +197,3 @@ router.delete( export default router; - - diff --git a/backend/src/routes/v2/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts index 254bf60f3c..11e8b1c716 100644 --- a/backend/src/routes/v2/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -30,13 +30,22 @@ router.post( acceptedRoles: [ADMIN, MEMBER], location: 'body' }), - body('name').exists().trim(), - body('workspaceId'), - body('environment'), - body('encryptedKey'), - body('iv'), - body('tag'), - body('expiresIn'), // measured in ms + body('name').exists().isString().trim(), + body('workspaceId').exists().isString().trim(), + body('environment').exists().isString().trim(), + body('encryptedKey').exists().isString().trim(), + body('iv').exists().isString().trim(), + body('tag').exists().isString().trim(), + body('expiresIn').exists().isNumeric(), // measured in ms + body('permissions').isArray({ min: 1 }).custom((value: string[]) => { + const allowedPermissions = ['read', 'write']; + const invalidValues = value.filter((v) => !allowedPermissions.includes(v)); + if (invalidValues.length > 0) { + throw new Error(`permissions contains invalid values: ${invalidValues.join(', ')}`); + } + + return true + }), validateRequest, serviceTokenDataController.createServiceTokenData ); diff --git a/backend/swagger/index.ts b/backend/swagger/index.ts index 9f4d4732cc..195a8198c0 100644 --- a/backend/swagger/index.ts +++ b/backend/swagger/index.ts @@ -197,6 +197,23 @@ const generateOpenAPISpec = async () => { secretValueCiphertext: '', secretValueIV: '', secretValueTag: '', + }, + ServiceTokenData: { + _id: '', + name: '', + workspace: '', + environment: '', + user: { + _id: '', + firstName: '', + lastName: '' + }, + expiresAt: '2023-01-13T14:16:12.210Z', + encryptedKey: '', + iv: '', + tag: '', + updatedAt: '2023-01-13T14:16:12.210Z', + createdAt: '2023-01-13T14:16:12.210Z' } } }; diff --git a/docs/api-reference/endpoints/service-tokens/get.mdx b/docs/api-reference/endpoints/service-tokens/get.mdx new file mode 100644 index 0000000000..ad3afbdda7 --- /dev/null +++ b/docs/api-reference/endpoints/service-tokens/get.mdx @@ -0,0 +1,4 @@ +--- +title: "Get" +openapi: "GET /api/v2/service-token/" +--- diff --git a/docs/api-reference/overview/authentication.mdx b/docs/api-reference/overview/authentication.mdx index 9c155a96ad..7df8f10e57 100644 --- a/docs/api-reference/overview/authentication.mdx +++ b/docs/api-reference/overview/authentication.mdx @@ -2,14 +2,24 @@ title: "Authentication" --- -To authenticate requests with Infisical, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. You can obtain an API key in User Settings > API Keys +To authenticate requests with Infisical, you can either use an API Key or [Infisical Token](../../../getting-started/dashboard/token); certain endpoints will accept either one or both. +- API Key: This general-purpose authentication token provides user access to most endpoints in this reference. +- [Infisical Token](../../../getting-started/dashboard/token): This authentication token (also referred to as the service token) is scoped to a specific project and environment and used for CRUD secret operations. + + + +To authenticate requests with Infisical using the API Key, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. + +You can obtain an API key in User Settings > API Keys ![API key dashboard](../../images/api-key-dashboard.png) ![API key in personal settings](../../images/api-key-settings.png) -![Adding an API key](../../images/api-key-add.png) + + +To authenticate requests with Infisical using the Infisical Token, you must include your Infisical Token in the `Authorization` header of HTTP requests made to the platform with the value `Bearer st.`. + +You can obtain an Infisical Token in Project Settings > Service Tokens. - - It's important to keep your API key secure, as it grants access to your - secrets in Infisical. For added security, set a reasonable expiration time and - rotate your API key on a regular basis. - +![token add](../../images/project-token-add.png) + + \ No newline at end of file diff --git a/docs/api-reference/overview/examples/create-secrets.mdx b/docs/api-reference/overview/examples/create-secrets.mdx index 0cd731f5ff..ab5b39aea7 100644 --- a/docs/api-reference/overview/examples/create-secrets.mdx +++ b/docs/api-reference/overview/examples/create-secrets.mdx @@ -2,49 +2,48 @@ title: "Create secrets" --- -In this example, we demonstrate how to add secrets to a project and environment. +In this example, we demonstrate how to add secrets to a project and environment using an Infisical Token. Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) +- Create an [Infisical Token](../../../getting-started/dashboard/token) for your project and environment. - Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). ## Flow -1. [Get your (encrypted) private key](/api-reference/endpoints/users/me). -2. Decrypt your (encrypted) private key with your password. -3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key) -4. Decrypt the (encrypted) project key with your private key. -5. Encrypt your secret(s) with the project key. -6. [Send (encrypted) secret(s) to the Infical API](/api-reference/endpoints/secrets/create) +1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key. +2. Decrypt the (encrypted) project key with the key from your Infisical Token. +3. Encrypt your secret(s) with the project key +4. [Send (encrypted) secret(s) to Infical](/api-reference/endpoints/secrets/create) ## Example + + ```js const crypto = require('crypto'); const axios = require('axios'); const nacl = require('tweetnacl'); +const BASE_URL = 'https://app.infisical.com'; const ALGORITHM = 'aes-256-gcm'; const BLOCK_SIZE_BYTES = 16; -const encrypt = ( - text, - secret -) => { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); - return { - ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') - }; +const encrypt = ({ text, secret }) => { + const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); + const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); + + let ciphertext = cipher.update(text, 'utf8', 'base64'); + ciphertext += cipher.final('base64'); + return { + ciphertext, + iv: iv.toString('base64'), + tag: cipher.getAuthTag().toString('base64') + }; } -const decrypt = (ciphertext, iv, tag, secret) => { +const decrypt = ({ ciphertext, iv, tag, secret}) => { const decipher = crypto.createDecipheriv( ALGORITHM, secret, @@ -59,95 +58,96 @@ const decrypt = (ciphertext, iv, tag, secret) => { } const createSecrets = async () => { - const API_KEY = 'your_api_key'; - const PSWD = 'your_pswd'; - const WORKSPACE_ID = 'your_workspace_id'; - - const 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(encryptedProjectKey.nonce), - util.decodeBase64(encryptedProjectKey.sender.publicKey), - util.decodeBase64(PSWD) - ); - - // 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 - } - } - ); + const serviceToken = ''; + const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); + + const secretType = 'shared'; // 'shared' or 'personal' + const secretKey = 'some_key'; + const secretValue = 'some_value'; + const secretComment = 'some_comment'; + + // 1. Get your Infisical Token data + const { data: serviceTokenData } = await axios.get( + `${BASE_URL}/api/v2/service-token`, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); + + // 2. Decrypt the (encrypted) project key with the key from your Infisical Token + const projectKey = decrypt({ + ciphertext: serviceTokenData.encryptedKey, + iv: serviceTokenData.iv, + tag: serviceTokenData.tag, + secret: serviceTokenSecret + }); + + // 3. Encrypt your secret(s) with the project key + const { + ciphertext: secretKeyCiphertext, + iv: secretKeyIV, + tag: secretKeyTag + } = encrypt({ + text: secretKey, + secret: projectKey + }); + + const { + ciphertext: secretValueCiphertext, + iv: secretValueIV, + tag: secretValueTag + } = encrypt({ + text: secretValue, + secret: projectKey + }); + + const { + ciphertext: secretCommentCiphertext, + iv: secretCommentIV, + tag: secretCommentTag + } = encrypt({ + text: secretComment, + secret: projectKey + }); + + const secret = { + type: secretType, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag + } + + // 4. Send (encrypted) secret(s) to Infisical + await axios.post( + `${BASE_URL}/api/v2/secrets`, + { + workspaceId: serviceTokenData.workspace, + environment: serviceTokenData.environment, + secrets: [secret] + }, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); } createSecrets(); ``` + + 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. - + \ No newline at end of file diff --git a/docs/api-reference/overview/examples/delete-secrets.mdx b/docs/api-reference/overview/examples/delete-secrets.mdx index c602cef7e3..312900a63b 100644 --- a/docs/api-reference/overview/examples/delete-secrets.mdx +++ b/docs/api-reference/overview/examples/delete-secrets.mdx @@ -7,24 +7,32 @@ In this example, we demonstrate how to delete secrets Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) +- Create either an [API Key](/api-reference/overview/authentication) or [Infisical Token](../../../getting-started/dashboard/token) for your project and environment. - Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). ## Example + + + + ```js +const axios = require('axios'); +const BASE_URL = 'https://app.infisical.com'; + const deleteSecrets = async () => { - const API_KEY = "your_api_key"; - const SECRET_ID = "ID"; // ID of secret to delete + const serviceToken = 'your_service_token'; + const secretId = 'id_of_secret_to_delete'; // 6. Send ID(s) of secret(s) to delete to the Infisical API await axios.delete( - `https://api.infisical.com/api/v2/secrets`, + `${BASE_URL}/api/v2/secrets`, { - secretIds: SECRET_ID, + secretIds: [secretId], }, { headers: { - "X-API-KEY": API_KEY, + Authorization: `Bearer ${serviceToken}` }, } ); @@ -32,3 +40,8 @@ const deleteSecrets = async () => { deleteSecrets(); ``` + + + If using an `API_KEY` to authenticate with the Infisical API, then you should include it in the `X_API_KEY` header. + + diff --git a/docs/api-reference/overview/examples/retrieve-secrets.mdx b/docs/api-reference/overview/examples/retrieve-secrets.mdx index 19d2d777b2..1ac4edb0c0 100644 --- a/docs/api-reference/overview/examples/retrieve-secrets.mdx +++ b/docs/api-reference/overview/examples/retrieve-secrets.mdx @@ -2,48 +2,33 @@ title: "Retrieve secrets" --- -In this example, we demonstrate how to retrieve secrets from a project and environment. +In this example, we demonstrate how to retrieve secrets from a project and environment using an Infisical Token. Prerequisites: -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) +- Set up and add envars to [Infisical Cloud](https://app.infisical.com). +- Create an [Infisical Token](../../../getting-started/dashboard/token) for your project and environment. - Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). ## Flow -1. [Get your (encrypted) private key.](/api-reference/endpoints/users/me) -2. Decrypt your (encrypted) private key with your password. -3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key) -4. Decrypt the (encrypted) project key with your private key. -5. [Get secrets for a project and environment.](/api-reference/endpoints/secrets/read) -6. Decrypt the (encrypted) secrets +1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key. +2. [Get secrets for your project and environment](/api-reference/endpoints/secrets/read). +3. Decrypt the (encrypted) project key with the key from your Infisical Token. +4. Decrypt the (encrypted) secrets ## Example + + ```js const crypto = require('crypto'); const axios = require('axios'); +const BASE_URL = 'https://app.infisical.com'; const ALGORITHM = 'aes-256-gcm'; -const BLOCK_SIZE_BYTES = 16; - -const encrypt = ( - text, - secret -) => { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); - return { - ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') - }; -} -const decrypt = (ciphertext, iv, tag, secret) => { +const decrypt = ({ ciphertext, iv, tag, secret}) => { const decipher = crypto.createDecipheriv( ALGORITHM, secret, @@ -51,73 +36,63 @@ const decrypt = (ciphertext, iv, tag, secret) => { ); decipher.setAuthTag(Buffer.from(tag, 'base64')); - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); + let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); + cleartext += decipher.final('utf8'); - return cleartext; + return cleartext; } -const retrieveSecrets = async () => { - const API_KEY = 'your_api_key'; - const PSWD = 'your_pswd'; - const WORKSPACE_ID = 'your_workspace_id'; - - // 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 - } - } - ); +const getSecrets = async () => { + const serviceToken = 'your_service_token'; + const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); + + // 1. Get your Infisical Token data + const { data: serviceTokenData } = await axios.get( + `${BASE_URL}/api/v2/service-token`, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); + + // 2. Get secrets for your project and environment + const { data } = await axios.get( + `${BASE_URL}/api/v2/secrets?${new URLSearchParams({ + environment: serviceTokenData.environment, + workspaceId: serviceTokenData.workspace + })}`, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); + + const encryptedSecrets = data.secrets; + + // 3. Decrypt the (encrypted) project key with the key from your Infisical Token + const projectKey = decrypt({ + ciphertext: serviceTokenData.encryptedKey, + iv: serviceTokenData.iv, + tag: serviceTokenData.tag, + secret: serviceTokenSecret + }); - // 6. decrypt the (encrypted) secrets - const secrets = encryptedSecrets.map((encryptedSecret) => { + // 4. Decrypt the (encrypted) secrets + const secrets = encryptedSecrets.map((secret) => { const secretKey = decrypt({ - ciphertext: encryptedSecret.secretKeyCiphertext, - iv: encryptedSecret.secretKeyIV, - tag: encryptedSecret.secretKeyTag - secret: projectKey + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + secret: projectKey }); + const secretValue = decrypt({ - ciphertext: encryptedSecret.secretValueCiphertext, - iv: encryptedSecret.secretValueIV, - tag: encryptedSecret.secretValueTag - secret: projectKey + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + secret: projectKey }); return ({ @@ -125,18 +100,18 @@ const retrieveSecrets = async () => { secretValue }); }); + + console.log('secrets: ', secrets); } -retrieveSecrets(); +getSecrets(); + ``` + + 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. - + \ No newline at end of file diff --git a/docs/api-reference/overview/examples/update-secrets.mdx b/docs/api-reference/overview/examples/update-secrets.mdx index 99e3530ee1..b3e16b2e33 100644 --- a/docs/api-reference/overview/examples/update-secrets.mdx +++ b/docs/api-reference/overview/examples/update-secrets.mdx @@ -2,48 +2,47 @@ title: "Update secrets" --- -In this example, we demonstrate how to update secrets +In this example, we demonstrate how to update secrets using an Infisical Token. Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) +- Create an [Infisical Token](../../../getting-started/dashboard/token) for your project and environment. - Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). ## Flow -1. [Get your (encrypted) private key.](/api-reference/endpoints/users/me) -2. Decrypt your (encrypted) private key with your password. -3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key) -4. Decrypt the (encrypted) project key with your private key. -5. Encrypt your secret(s) with the project key. -6. [Send (encrypted) updated secret(s) to the Infical API.](/api-reference/endpoints/secrets/update) +1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key. +2. Decrypt the (encrypted) project key with the key from your Infisical Token. +3. Encrypt your updated secret(s) with the project key +4. [Send (encrypted) updated secret(s) to Infical](/api-reference/endpoints/secrets/update) ## Example + + ```js const crypto = require('crypto'); const axios = require('axios'); +const BASE_URL = 'https://app.infisical.com'; const ALGORITHM = 'aes-256-gcm'; const BLOCK_SIZE_BYTES = 16; -const encrypt = ( - text, - secret -) => { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); - return { - ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') - }; +const encrypt = ({ text, secret }) => { + const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); + const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); + + let ciphertext = cipher.update(text, 'utf8', 'base64'); + ciphertext += cipher.final('base64'); + return { + ciphertext, + iv: iv.toString('base64'), + tag: cipher.getAuthTag().toString('base64') + }; } -const decrypt = (ciphertext, iv, tag, secret) => { +const decrypt = ({ ciphertext, iv, tag, secret}) => { const decipher = crypto.createDecipheriv( ALGORITHM, secret, @@ -58,95 +57,96 @@ const decrypt = (ciphertext, iv, tag, secret) => { } const updateSecrets = async () => { - const API_KEY = 'your_api_key'; - const PSWD = 'your_pswd'; - const WORKSPACE_ID = 'your_workspace_id'; - - const 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 - } - } - ); + const serviceToken = 'your_service_token'; + const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); + + const secretId = 'id_of_secret_to_update'; + const secretKey = 'some_key'; + const secretValue = 'updated_value'; + const secretComment = 'updated_comment'; + + // 1. Get your Infisical Token data + const { data: serviceTokenData } = await axios.get( + `${BASE_URL}/api/v2/service-token`, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); + + // 2. Decrypt the (encrypted) project key with the key from your Infisical Token + const projectKey = decrypt({ + ciphertext: serviceTokenData.encryptedKey, + iv: serviceTokenData.iv, + tag: serviceTokenData.tag, + secret: serviceTokenSecret + }); + + // 3. Encrypt your updated secret(s) with the project key + const { + ciphertext: secretKeyCiphertext, + iv: secretKeyIV, + tag: secretKeyTag + } = encrypt({ + text: secretKey, + secret: projectKey + }); + + const { + ciphertext: secretValueCiphertext, + iv: secretValueIV, + tag: secretValueTag + } = encrypt({ + text: secretValue, + secret: projectKey + }); + + const { + ciphertext: secretCommentCiphertext, + iv: secretCommentIV, + tag: secretCommentTag + } = encrypt({ + text: secretComment, + secret: projectKey + }); + + const secret = { + id: secretId, + workspace: serviceTokenData.workspace, + environment: serviceTokenData.environment, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag + } + + // 4. Send (encrypted) updated secret(s) to Infisical + await axios.patch( + `${BASE_URL}/api/v2/secrets`, + { + secrets: [secret] + }, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); } updateSecrets(); ``` + + 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. - + \ No newline at end of file diff --git a/docs/api-reference/overview/introduction.mdx b/docs/api-reference/overview/introduction.mdx index 9865a644a4..3d748314b2 100644 --- a/docs/api-reference/overview/introduction.mdx +++ b/docs/api-reference/overview/introduction.mdx @@ -12,10 +12,11 @@ With the REST API, users can create, read, update, and delete secrets, as well a Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview). -- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by the user's password before being sent off to the server during the account signup process. +- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by protected keys that are encrypted by keys derived from Argon2id applied to the user's password before being sent off to the server during the account signup process. - Each (encrypted) secret belongs to a project and environment. - Each project has an (encrypted) project key used to encrypt the secrets within that project; Infisical stores copies of the project key, for each member of that project, encrypted under each member's public key. - Secrets are encrypted symmetrically by your copy of the project key belonging to the project containing. +- Infisical Tokens contain a symmetric key that can be used to decrypt a copy of a project key from the [call to get the Infisical Token data](/api-reference/endpoints/service-tokens/get). - Infisical uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations. diff --git a/docs/mint.json b/docs/mint.json index 4ac6ef3db1..7903524f61 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -249,6 +249,12 @@ "api-reference/endpoints/secrets/versions", "api-reference/endpoints/secrets/rollback-version" ] + }, + { + "group": "Service Tokens", + "pages": [ + "api-reference/endpoints/service-tokens/get" + ] } ] }, diff --git a/docs/spec.yaml b/docs/spec.yaml index 3d5ca4d7d5..b57caf89ac 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -309,78 +309,6 @@ paths: example: any code: example: any - /api/v1/signup/complete-account/signup: - post: - description: '' - parameters: [] - responses: - '200': - description: OK - '400': - description: Bad Request - '403': - description: Forbidden - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - example: any - firstName: - example: any - lastName: - example: any - publicKey: - example: any - encryptedPrivateKey: - example: any - iv: - example: any - tag: - example: any - salt: - example: any - verifier: - example: any - organizationName: - example: any - /api/v1/signup/complete-account/invite: - post: - description: '' - parameters: [] - responses: - '200': - description: OK - '400': - description: Bad Request - '403': - description: Forbidden - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - example: any - firstName: - example: any - lastName: - example: any - publicKey: - example: any - encryptedPrivateKey: - example: any - iv: - example: any - tag: - example: any - salt: - example: any - verifier: - example: any /api/v1/auth/token: post: description: '' @@ -412,7 +340,11 @@ paths: /api/v1/auth/login2: post: description: '' - parameters: [] + parameters: + - name: user-agent + in: header + schema: + type: string responses: '200': description: OK @@ -431,7 +363,11 @@ paths: /api/v1/auth/logout: post: description: '' - parameters: [] + parameters: + - name: user-agent + in: header + schema: + type: string responses: '200': description: OK @@ -691,6 +627,18 @@ paths: description: OK '400': description: Bad Request + /api/v1/organization/{organizationId}/workspace-memberships: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK /api/v1/workspace/{workspaceId}/keys: get: description: '' @@ -933,6 +881,26 @@ paths: properties: role: example: any + /api/v1/membership/{membershipId}/deny-permissions: + post: + description: '' + parameters: + - name: membershipId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + type: object + properties: + permissions: + example: any /api/v1/key/{workspaceId}: post: description: '' @@ -1147,11 +1115,17 @@ paths: properties: clientProof: example: any + protectedKey: + example: any + protectedKeyIV: + example: any + protectedKeyTag: + example: any encryptedPrivateKey: example: any - iv: + encryptedPrivateKeyIV: example: any - tag: + encryptedPrivateKeyTag: example: any salt: example: any @@ -1247,11 +1221,17 @@ paths: schema: type: object properties: + protectedKey: + example: any + protectedKeyIV: + example: any + protectedKeyTag: + example: any encryptedPrivateKey: example: any - iv: + encryptedPrivateKeyIV: example: any - tag: + encryptedPrivateKeyTag: example: any salt: example: any @@ -1270,6 +1250,39 @@ paths: description: OK '400': description: Bad Request + /api/v1/integration/: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + integrationAuthId: + example: any + app: + example: any + appId: + example: any + isActive: + example: any + sourceEnvironment: + example: any + targetEnvironment: + example: any + owner: + example: any + path: + example: any + region: + example: any /api/v1/integration/{integrationId}: patch: description: '' @@ -1290,17 +1303,17 @@ paths: schema: type: object properties: - app: - example: any environment: example: any isActive: example: any - target: + app: + example: any + appId: example: any - context: + targetEnvironment: example: any - siteId: + owner: example: any delete: description: '' @@ -1322,6 +1335,33 @@ paths: responses: '200': description: OK + /api/v1/integration-auth/{integrationAuthId}: + get: + description: '' + parameters: + - name: integrationAuthId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + delete: + description: '' + parameters: + - name: integrationAuthId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request /api/v1/integration-auth/oauth-token: post: description: '' @@ -1343,6 +1383,29 @@ paths: example: any integration: example: any + /api/v1/integration-auth/access-token: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + workspaceId: + example: any + accessId: + example: any + accessToken: + example: any + integration: + example: any /api/v1/integration-auth/{integrationAuthId}/apps: get: description: '' @@ -1357,13 +1420,115 @@ paths: description: OK '400': description: Bad Request - /api/v1/integration-auth/{integrationAuthId}: - delete: + /api/v2/signup/complete-account/signup: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + '403': + description: Forbidden + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + firstName: + example: any + lastName: + example: any + protectedKey: + example: any + protectedKeyIV: + example: any + protectedKeyTag: + example: any + publicKey: + example: any + encryptedPrivateKey: + example: any + encryptedPrivateKeyIV: + example: any + encryptedPrivateKeyTag: + example: any + salt: + example: any + verifier: + example: any + organizationName: + example: any + /api/v2/signup/complete-account/invite: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + '403': + description: Forbidden + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + firstName: + example: any + lastName: + example: any + protectedKey: + example: any + protectedKeyIV: + example: any + protectedKeyTag: + example: any + publicKey: + example: any + encryptedPrivateKey: + example: any + encryptedPrivateKeyIV: + example: any + encryptedPrivateKeyTag: + example: any + salt: + example: any + verifier: + example: any + /api/v2/auth/login1: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + clientPublicKey: + example: any + /api/v2/auth/login2: + post: description: '' parameters: - - name: integrationAuthId - in: path - required: true + - name: user-agent + in: header schema: type: string responses: @@ -1371,6 +1536,54 @@ paths: description: OK '400': description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + clientProof: + example: any + /api/v2/auth/mfa/send: + post: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + /api/v2/auth/mfa/verify: + post: + description: '' + parameters: + - name: user-agent + in: header + schema: + type: string + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + example: any + mfaToken: + example: any /api/v2/users/me: get: summary: Retrieve the current user on the request @@ -1392,6 +1605,23 @@ paths: description: Bad Request security: - apiKeyAuth: [] + /api/v2/users/me/mfa: + patch: + description: '' + parameters: [] + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + isMfaEnabled: + example: any /api/v2/users/me/organizations: get: summary: Return organizations that current user is part of @@ -1615,6 +1845,62 @@ paths: properties: environmentSlug: example: any + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v2/workspace/{workspaceId}/tags: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + post: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + example: any + slug: + example: any + /api/v2/workspace/tags/{tagId}: + delete: + description: '' + parameters: + - name: tagId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK /api/v2/workspace/{workspaceId}/secrets: post: description: '' @@ -1804,6 +2090,28 @@ paths: description: Bad Request security: - apiKeyAuth: [] + /api/v2/workspace/{workspaceId}/auto-capitalization: + patch: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + requestBody: + content: + application/json: + schema: + type: object + properties: + autoCapitalization: + example: any /api/v2/secret/batch-create/workspace/{workspaceId}/environment/{environment}: post: description: '' @@ -1968,11 +2276,38 @@ paths: properties: secret: example: any + /api/v2/secrets/batch: + post: + description: '' + parameters: + - name: user-agent + in: header + schema: + type: string + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + type: object + properties: + workspaceId: + example: any + environment: + example: any + requests: + example: any /api/v2/secrets/: post: summary: Create new secret(s) description: Create one or many secrets for a given project and environment. - parameters: [] + parameters: + - name: user-agent + in: header + schema: + type: string responses: '200': description: OK @@ -2022,6 +2357,10 @@ paths: in: query schema: type: string + - name: user-agent + in: header + schema: + type: string - name: content in: query schema: @@ -2073,7 +2412,11 @@ paths: delete: summary: Delete secret(s) description: Delete one or many secrets by their ID(s) - parameters: [] + parameters: + - name: user-agent + in: header + schema: + type: string responses: '200': description: OK @@ -2101,13 +2444,23 @@ paths: description: ID(s) of secrets - string or array of strings /api/v2/service-token/: get: - description: '' + summary: Return Infisical Token data + description: Return Infisical Token data parameters: [] responses: '200': description: OK - '400': - description: Bad Request + content: + application/json: + schema: + type: object + properties: + serviceTokenData: + type: object + $ref: '#/components/schemas/ServiceTokenData' + description: Details of service token + security: + - bearerAuth: [] post: description: '' parameters: [] @@ -2136,6 +2489,8 @@ paths: example: any expiresIn: example: any + permissions: + example: any /api/v2/service-token/{serviceTokenDataId}: delete: description: '' @@ -2317,9 +2672,6 @@ components: Project: type: object properties: - _id: - type: string - example: '' name: type: string example: My Project @@ -2602,6 +2954,51 @@ components: secretValueTag: type: string example: '' + ServiceTokenData: + type: object + properties: + _id: + type: string + example: '' + name: + type: string + example: '' + workspace: + type: string + example: '' + environment: + type: string + example: '' + user: + type: object + properties: + _id: + type: string + example: '' + firstName: + type: string + example: '' + lastName: + type: string + example: '' + expiresAt: + type: string + example: '2023-01-13T14:16:12.210Z' + encryptedKey: + type: string + example: '' + iv: + type: string + example: '' + tag: + type: string + example: '' + updatedAt: + type: string + example: '2023-01-13T14:16:12.210Z' + createdAt: + type: string + example: '2023-01-13T14:16:12.210Z' securitySchemes: bearerAuth: type: http diff --git a/frontend/next-i18next.config.js b/frontend/next-i18next.config.js index 866f85b2c7..e8046b755e 100644 --- a/frontend/next-i18next.config.js +++ b/frontend/next-i18next.config.js @@ -23,4 +23,4 @@ module.exports = { // strictMode: true, // serializeConfig: false, // react: { useSuspense: false } -}; +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/serviceTokens/queries.tsx b/frontend/src/hooks/api/serviceTokens/queries.tsx index 313daa8610..9817732806 100644 --- a/frontend/src/hooks/api/serviceTokens/queries.tsx +++ b/frontend/src/hooks/api/serviceTokens/queries.tsx @@ -37,7 +37,7 @@ export const useCreateServiceToken = () => { return useMutation({ mutationFn: async (body) => { const { data } = await apiRequest.post('/api/v2/service-token/', body); - data.serviceToken += `.${ body.randomBytes}`; + data.serviceToken += `.${body.randomBytes}`; return data; }, onSuccess: ({ serviceTokenData: { workspace } }) => { diff --git a/frontend/src/hooks/api/serviceTokens/types.ts b/frontend/src/hooks/api/serviceTokens/types.ts index 28bb55b4b3..83bad9db6b 100644 --- a/frontend/src/hooks/api/serviceTokens/types.ts +++ b/frontend/src/hooks/api/serviceTokens/types.ts @@ -19,6 +19,7 @@ export type CreateServiceTokenDTO = { iv: string; tag: string; randomBytes: string; + permissions: string[]; }; export type CreateServiceTokenRes = { diff --git a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx index 69a4dd8d5b..af812c6aac 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx @@ -203,7 +203,7 @@ export const ProjectSettingsPage = () => { } }; - const onCreateServiceToken = async ({ environment, expiresIn, name }: CreateServiceToken) => { + const onCreateServiceToken = async ({ environment, expiresIn, name, permissions }: CreateServiceToken) => { // type guard if (!latestFileKey) return ''; try { @@ -219,7 +219,7 @@ export const ProjectSettingsPage = () => { plaintext: key, key: randomBytes }); - + const res = await createServiceToken.mutateAsync({ encryptedKey: ciphertext, iv, @@ -228,9 +228,12 @@ export const ProjectSettingsPage = () => { expiresIn: Number(expiresIn), name, workspaceId: workspaceID, - randomBytes + randomBytes, + permissions: Object.entries(permissions) + .filter(([, permissionsValue]) => permissionsValue) + .map(([permissionsKey]) => permissionsKey) }); - console.log(res); + createNotification({ text: 'Successfully created a service token', type: 'success' diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx index e4c4bbf2e5..4298764358 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx @@ -8,6 +8,7 @@ import * as yup from 'yup'; import { Button, + Checkbox, DeleteActionModal, EmptyState, FormControl, @@ -42,7 +43,11 @@ const apiTokenExpiry = [ const createServiceTokenSchema = yup.object({ name: yup.string().required().label('Service Token Name'), environment: yup.string().required().label('Environment'), - expiresIn: yup.string().required().label('Service Token Name') + expiresIn: yup.string().required().label('Service Token Name'), + permissions: yup.object().shape({ + read: yup.boolean().required(), + write: yup.boolean().required() + }).defined().required() }); export type CreateServiceToken = yup.InferType; @@ -87,7 +92,7 @@ export const ServiceTokenSection = ({ 'createAPIToken', 'deleteAPITokenConfirmation' ] as const); - + const { control, reset, @@ -216,6 +221,90 @@ export const ServiceTokenSection = ({ )} /> + { + const options = [{ + label: 'Read (default)', + value: 'read' + }, { + label: 'Write (optional)', + value: 'write' + }]; + + return ( + + <> + {options.map(({ label, value: optionValue }) => { + // TODO: refactor + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} + + + ); + }} + /> + {/* { + return ( + { + onChange(state); + }} + > + Read (default) + + ); + }} + /> + { + return ( + { + onChange(state); + }} + > + Write (optional) + + ); + }} + /> */}