Merge pull request #225 from Infisical/expand-open-api

Add new organization endpoints to API reference
This commit is contained in:
BlackMagiq
2023-01-16 00:04:08 +07:00
committed by GitHub
28 changed files with 1546 additions and 199 deletions
+511 -53
View File
@@ -2258,6 +2258,368 @@
] ]
} }
}, },
"/api/v2/users/me/organizations": {
"get": {
"summary": "Return organizations that current user is part of",
"description": "Return organizations that current user is part of",
"parameters": [],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"organizations": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Organization"
},
"description": "Organizations that user is part of"
}
}
}
}
}
},
"400": {
"description": "Bad Request"
}
},
"security": [
{
"apiKeyAuth": []
}
]
}
},
"/api/v2/organizations/{organizationId}/memberships": {
"get": {
"summary": "Return organization memberships",
"description": "Return organization memberships",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "ID of organization"
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"memberships": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MembershipOrg"
},
"description": "Memberships of organization"
}
}
}
}
}
},
"400": {
"description": "Bad Request"
}
},
"security": [
{
"apiKeyAuth": []
}
]
}
},
"/api/v2/organizations/{organizationId}/memberships/{membershipId}": {
"patch": {
"summary": "Update organization membership",
"description": "Update organization membership",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "ID of organization"
},
{
"name": "membershipId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "ID of organization membership to update"
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membership": {
"$ref": "#/components/schemas/MembershipOrg",
"description": "Updated organization membership"
}
}
}
}
}
},
"400": {
"description": "Bad Request"
}
},
"security": [
{
"apiKeyAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"role": {
"type": "string",
"description": "Role of organization membership - either owner, admin, or member"
}
}
}
}
}
}
},
"delete": {
"summary": "Delete organization membership",
"description": "Delete organization membership",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "ID of organization"
},
{
"name": "membershipId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "ID of organization membership to delete"
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membership": {
"$ref": "#/components/schemas/MembershipOrg",
"description": "Deleted organization membership"
}
}
}
}
}
},
"400": {
"description": "Bad Request"
}
},
"security": [
{
"apiKeyAuth": []
}
]
}
},
"/api/v2/organizations/{organizationId}/workspaces": {
"get": {
"summary": "Return projects in organization that user is part of",
"description": "Return projects in organization that user is part of",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "ID of organization"
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaces": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Project"
},
"description": "Projects of organization"
}
}
}
}
}
},
"400": {
"description": "Bad Request"
}
},
"security": [
{
"apiKeyAuth": []
}
]
}
},
"/api/v2/workspace/{workspaceId}/environments": {
"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": {
"environmentName": {
"example": "any"
},
"environmentSlug": {
"example": "any"
}
}
}
}
}
}
},
"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": [
{
"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": {
"environmentSlug": {
"example": "any"
}
}
}
}
}
}
}
},
"/api/v2/workspace/{workspaceId}/secrets": { "/api/v2/workspace/{workspaceId}/secrets": {
"post": { "post": {
"description": "", "description": "",
@@ -2450,56 +2812,6 @@
} }
}, },
"/api/v2/workspace/{workspaceId}/memberships/{membershipId}": { "/api/v2/workspace/{workspaceId}/memberships/{membershipId}": {
"delete": {
"summary": "Delete project membership",
"description": "Delete project membership",
"parameters": [
{
"name": "workspaceId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "ID of project"
},
{
"name": "membershipId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "ID of membership"
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membership": {
"$ref": "#/components/schemas/Membership",
"description": "Deleted membership"
}
}
}
}
}
},
"400": {
"description": "Bad Request"
}
},
"security": [
{
"apiKeyAuth": []
}
]
},
"patch": { "patch": {
"summary": "Update project membership", "summary": "Update project membership",
"description": "Update project membership", "description": "Update project membership",
@@ -2520,7 +2832,7 @@
"schema": { "schema": {
"type": "string" "type": "string"
}, },
"description": "ID of membership" "description": "ID of project membership to update"
} }
], ],
"responses": { "responses": {
@@ -2565,6 +2877,56 @@
} }
} }
} }
},
"delete": {
"summary": "Delete project membership",
"description": "Delete project membership",
"parameters": [
{
"name": "workspaceId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "ID of project"
},
{
"name": "membershipId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "ID of project membership to delete"
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membership": {
"$ref": "#/components/schemas/Membership",
"description": "Deleted membership"
}
}
}
}
}
},
"400": {
"description": "Bad Request"
}
},
"security": [
{
"apiKeyAuth": []
}
]
} }
}, },
"/api/v2/secret/batch-create/workspace/{workspaceId}/environment/{environment}": { "/api/v2/secret/batch-create/workspace/{workspaceId}/environment/{environment}": {
@@ -3131,7 +3493,7 @@
} }
} }
}, },
"/api/v2/api-key-data/": { "/api/v2/api-key/": {
"get": { "get": {
"description": "", "description": "",
"parameters": [], "parameters": [],
@@ -3174,7 +3536,7 @@
} }
} }
}, },
"/api/v2/api-key-data/{apiKeyDataId}": { "/api/v2/api-key/{apiKeyDataId}": {
"delete": { "delete": {
"description": "", "description": "",
"parameters": [ "parameters": [
@@ -3302,6 +3664,102 @@
} }
} }
}, },
"MembershipOrg": {
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"_id": {
"type": "string",
"example": ""
},
"email": {
"type": "string",
"example": ""
},
"firstName": {
"type": "string",
"example": ""
},
"lastName": {
"type": "string",
"example": ""
},
"publicKey": {
"type": "string",
"example": ""
},
"updatedAt": {
"type": "string",
"example": ""
},
"createdAt": {
"type": "string",
"example": ""
}
}
},
"organization": {
"type": "string",
"example": ""
},
"role": {
"type": "string",
"example": "owner"
},
"status": {
"type": "string",
"example": "accepted"
}
}
},
"Organization": {
"type": "object",
"properties": {
"_id": {
"type": "string",
"example": ""
},
"name": {
"type": "string",
"example": ""
},
"customerId": {
"type": "string",
"example": ""
}
}
},
"Project": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "My Project"
},
"organization": {
"type": "string",
"example": ""
},
"environments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "development"
},
"slug": {
"type": "string",
"example": "dev"
}
}
}
}
}
},
"ProjectKey": { "ProjectKey": {
"type": "object", "type": "object",
"properties": { "properties": {
+6 -5
View File
@@ -42,9 +42,10 @@ import {
} from './routes/v1'; } from './routes/v1';
import { import {
users as v2UsersRouter, users as v2UsersRouter,
organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter, secrets as v2SecretsRouter,
workspace as v2WorkspaceRouter,
serviceTokenData as v2ServiceTokenDataRouter, serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter, apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter, environment as v2EnvironmentRouter,
@@ -98,7 +99,7 @@ app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter); app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter); app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter); app.use('/api/v1/secret', v1SecretRouter);
app.use('/api/v1/service-token', v1ServiceTokenRouter); // stop supporting app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecated
app.use('/api/v1/password', v1PasswordRouter); app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter); app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter); app.use('/api/v1/integration', v1IntegrationRouter);
@@ -106,9 +107,10 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
// v2 routes // v2 routes
app.use('/api/v2/users', v2UsersRouter); app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter); app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter); app.use('/api/v2/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/api-key', v2APIKeyDataRouter); app.use('/api/v2/api-key', v2APIKeyDataRouter);
@@ -128,7 +130,6 @@ app.use((req, res, next) => {
//* Error Handling Middleware (must be after all routing logic) //* Error Handling Middleware (must be after all routing logic)
app.use(requestErrorHandler) app.use(requestErrorHandler)
export const server = app.listen(PORT, () => { export const server = app.listen(PORT, () => {
getLogger("backend-main").info(`Server started listening at port ${PORT}`) getLogger("backend-main").info(`Server started listening at port ${PORT}`)
}); });
@@ -29,12 +29,6 @@ const productToPriceMap = {
cardAuth: STRIPE_PRODUCT_CARD_AUTH cardAuth: STRIPE_PRODUCT_CARD_AUTH
}; };
/**
* Return organizations that user is part of
* @param req
* @param res
* @returns
*/
export const getOrganizations = async (req: Request, res: Response) => { export const getOrganizations = async (req: Request, res: Response) => {
let organizations; let organizations;
try { try {
+2
View File
@@ -1,4 +1,5 @@
import * as usersController from './usersController'; import * as usersController from './usersController';
import * as organizationsController from './organizationsController';
import * as workspaceController from './workspaceController'; import * as workspaceController from './workspaceController';
import * as serviceTokenDataController from './serviceTokenDataController'; import * as serviceTokenDataController from './serviceTokenDataController';
import * as apiKeyDataController from './apiKeyDataController'; import * as apiKeyDataController from './apiKeyDataController';
@@ -8,6 +9,7 @@ import * as environmentController from './environmentController';
export { export {
usersController, usersController,
organizationsController,
workspaceController, workspaceController,
serviceTokenDataController, serviceTokenDataController,
apiKeyDataController, apiKeyDataController,
@@ -0,0 +1,296 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
MembershipOrg,
Membership,
Workspace
} from '../../models';
import { deleteMembershipOrg } from '../../helpers/membershipOrg';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
/**
* Return memberships for organization with id [organizationId]
* @param req
* @param res
*/
export const getOrganizationMemberships = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return organization memberships'
#swagger.description = 'Return organization memberships'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"memberships": {
"type": "array",
"items": {
$ref: "#/components/schemas/MembershipOrg"
},
"description": "Memberships of organization"
}
}
}
}
}
}
*/
let memberships;
try {
const { organizationId } = req.params;
memberships = await MembershipOrg.find({
organization: organizationId
}).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization memberships'
});
}
return res.status(200).send({
memberships
});
}
/**
* Update role of membership with id [membershipId] to role [role]
* @param req
* @param res
*/
export const updateOrganizationMembership = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update organization membership'
#swagger.description = 'Update organization membership'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string"
}
#swagger.parameters['membershipId'] = {
"description": "ID of organization membership to update",
"required": true,
"type": "string"
}
#swagger.requestBody = {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"role": {
"type": "string",
"description": "Role of organization membership - either owner, admin, or member",
}
}
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membership": {
$ref: "#/components/schemas/MembershipOrg",
"description": "Updated organization membership"
}
}
}
}
}
}
*/
let membership;
try {
const { membershipId } = req.params;
const { role } = req.body;
membership = await MembershipOrg.findByIdAndUpdate(
membershipId,
{
role
}, {
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update organization membership'
});
}
return res.status(200).send({
membership
});
}
/**
* Delete organization membership with id [membershipId]
* @param req
* @param res
* @returns
*/
export const deleteOrganizationMembership = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete organization membership'
#swagger.description = 'Delete organization membership'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string"
}
#swagger.parameters['membershipId'] = {
"description": "ID of organization membership to delete",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membership": {
$ref: "#/components/schemas/MembershipOrg",
"description": "Deleted organization membership"
}
}
}
}
}
}
*/
let membership;
try {
const { membershipId } = req.params;
// delete organization membership
membership = await deleteMembershipOrg({
membershipOrgId: membershipId
});
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString()
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization membership'
});
}
return res.status(200).send({
membership
});
}
/**
* Return workspaces for organization with id [organizationId] that user has
* access to
* @param req
* @param res
*/
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return projects in organization that user is part of'
#swagger.description = 'Return projects in organization that user is part of'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaces": {
"type": "array",
"items": {
$ref: "#/components/schemas/Project"
},
"description": "Projects of organization"
}
}
}
}
}
}
*/
let workspaces;
try {
const { organizationId } = req.params;
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
'_id'
)
).map((w) => w._id.toString())
);
workspaces = (
await Membership.find({
user: req.user._id
}).populate('workspace')
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization workspaces'
});
}
return res.status(200).send({
workspaces
});
}
+62 -2
View File
@@ -1,9 +1,16 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { import {
User User,
MembershipOrg
} from '../../models'; } from '../../models';
/**
* Return the current user.
* @param req
* @param res
* @returns
*/
export const getMe = async (req: Request, res: Response) => { export const getMe = async (req: Request, res: Response) => {
/* /*
#swagger.summary = "Retrieve the current user on the request" #swagger.summary = "Retrieve the current user on the request"
@@ -39,11 +46,64 @@ export const getMe = async (req: Request, res: Response) => {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get user' message: 'Failed to get current user'
}); });
} }
return res.status(200).send({ return res.status(200).send({
user user
}); });
}
/**
* Return organizations that the current user is part of.
* @param req
* @param res
*/
export const getMyOrganizations = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return organizations that current user is part of'
#swagger.description = 'Return organizations that current user is part of'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"organizations": {
"type": "array",
"items": {
$ref: "#/components/schemas/Organization"
},
"description": "Organizations that user is part of"
}
}
}
}
}
}
*/
let organizations;
try {
organizations = (
await MembershipOrg.find({
user: req.user._id
}).populate('organization')
).map((m) => m.organization);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get current user's organizations"
});
}
return res.status(200).send({
organizations
});
} }
@@ -311,76 +311,6 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
}); });
} }
/**
* Delete workspace membership with id [membershipId]
* @param req
* @param res
* @returns
*/
export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete project membership'
#swagger.description = 'Delete project membership'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
#swagger.parameters['membershipId'] = {
"description": "ID of membership",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membership": {
$ref: "#/components/schemas/Membership",
"description": "Deleted membership"
}
}
}
}
}
}
*/
let membership;
try {
const {
membershipId
} = req.params;
membership = await Membership.findByIdAndDelete(membershipId);
if (!membership) throw new Error('Failed to delete workspace membership');
await Key.deleteMany({
receiver: membership.user,
workspace: membership.workspace
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace membership'
});
}
return res.status(200).send({
membership
});
}
/** /**
* Update role of membership with id [membershipId] to role [role] * Update role of membership with id [membershipId] to role [role]
* @param req * @param req
@@ -403,7 +333,7 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
} }
#swagger.parameters['membershipId'] = { #swagger.parameters['membershipId'] = {
"description": "ID of membership", "description": "ID of project membership to update",
"required": true, "required": true,
"type": "string" "type": "string"
} }
@@ -467,4 +397,74 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
return res.status(200).send({ return res.status(200).send({
membership membership
}); });
}
/**
* Delete workspace membership with id [membershipId]
* @param req
* @param res
* @returns
*/
export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete project membership'
#swagger.description = 'Delete project membership'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
#swagger.parameters['membershipId'] = {
"description": "ID of project membership to delete",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membership": {
$ref: "#/components/schemas/Membership",
"description": "Deleted membership"
}
}
}
}
}
}
*/
let membership;
try {
const {
membershipId
} = req.params;
membership = await Membership.findByIdAndDelete(membershipId);
if (!membership) throw new Error('Failed to delete workspace membership');
await Key.deleteMany({
receiver: membership.user,
workspace: membership.workspace
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace membership'
});
}
return res.status(200).send({
membership
});
} }
+1
View File
@@ -7,6 +7,7 @@ import { Membership, Key } from '../models';
* @param {Object} obj * @param {Object} obj
* @param {String} obj.userId - id of user to validate * @param {String} obj.userId - id of user to validate
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
*/ */
const validateMembership = async ({ const validateMembership = async ({
userId, userId,
+44 -1
View File
@@ -1,6 +1,42 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { MembershipOrg, Workspace, Membership, Key } from '../models'; import { MembershipOrg, Workspace, Membership, Key } from '../models';
/**
* Validate that user with id [userId] is a member of organization with id [organizationId]
* and has at least one of the roles in [acceptedRoles]
*
*/
const validateMembership = async ({
userId,
organizationId,
acceptedRoles
}: {
userId: string;
organizationId: string;
acceptedRoles: string[];
}) => {
let membership;
try {
membership = await MembershipOrg.findOne({
user: new Types.ObjectId(userId),
organization: new Types.ObjectId(organizationId)
});
if (!membership) throw new Error('Failed to find organization membership');
if (!acceptedRoles.includes(membership.role)) {
throw new Error('Failed to validate organization membership role');
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to validate organization membership');
}
return membership;
}
/** /**
* Return organization membership matching criteria specified in * Return organization membership matching criteria specified in
* query [queryObj] * query [queryObj]
@@ -84,6 +120,8 @@ const deleteMembershipOrg = async ({
_id: membershipOrgId _id: membershipOrgId
}); });
if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership');
// delete keys associated with organization membership // delete keys associated with organization membership
if (deletedMembershipOrg?.user) { if (deletedMembershipOrg?.user) {
// case: organization membership had a registered user // case: organization membership had a registered user
@@ -117,4 +155,9 @@ const deleteMembershipOrg = async ({
return deletedMembershipOrg; return deletedMembershipOrg;
}; };
export { findMembershipOrg, addMembershipsOrg, deleteMembershipOrg }; export {
validateMembership,
findMembershipOrg,
addMembershipsOrg,
deleteMembershipOrg
};
+2
View File
@@ -3,6 +3,7 @@ import requireBotAuth from './requireBotAuth';
import requireSignupAuth from './requireSignupAuth'; import requireSignupAuth from './requireSignupAuth';
import requireWorkspaceAuth from './requireWorkspaceAuth'; import requireWorkspaceAuth from './requireWorkspaceAuth';
import requireMembershipAuth from './requireMembershipAuth'; import requireMembershipAuth from './requireMembershipAuth';
import requireMembershipOrgAuth from './requireMembershipOrgAuth';
import requireOrganizationAuth from './requireOrganizationAuth'; import requireOrganizationAuth from './requireOrganizationAuth';
import requireIntegrationAuth from './requireIntegrationAuth'; import requireIntegrationAuth from './requireIntegrationAuth';
import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth'; import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth';
@@ -18,6 +19,7 @@ export {
requireSignupAuth, requireSignupAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
requireMembershipAuth, requireMembershipAuth,
requireMembershipOrgAuth,
requireOrganizationAuth, requireOrganizationAuth,
requireIntegrationAuth, requireIntegrationAuth,
requireIntegrationAuthorizationAuth, requireIntegrationAuthorizationAuth,
@@ -6,11 +6,12 @@ import {
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
/** /**
* Validate membership with id [membershipId] and that user with id * Validate membership with id [membershipId] and that user with id
* [req.user._id] can modify that membership. * [req.user._id] can modify that membership.
* @param {Object} obj * @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth * @param {String[]} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
*/ */
const requireMembershipAuth = ({ const requireMembershipAuth = ({
@@ -0,0 +1,49 @@
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import {
MembershipOrg
} from '../models';
import { validateMembership } from '../helpers/membershipOrg';
type req = 'params' | 'body' | 'query';
/**
* Validate (organization) membership id [membershipId] and that user with id
* [req.user._id] can modify that membership.
* @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted organization roles
* @param {String[]} obj.location - location of [membershipId] on request (e.g. params, body) for parsing
*/
const requireMembershipOrgAuth = ({
acceptedRoles,
location = 'params'
}: {
acceptedRoles: string[];
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { membershipId } = req[location];
const membershipOrg = await MembershipOrg.findById(membershipId);
if (!membershipOrg) throw new Error('Failed to find target organization membership');
const targetMembership = await validateMembership({
userId: req.user._id.toString(),
organizationId: membershipOrg.organization.toString(),
acceptedRoles
});
req.targetMembership = targetMembership;
return next();
} catch (err) {
return next(UnauthorizedRequestError({
message: 'Unable to validate organization membership'
}));
}
}
}
export default requireMembershipOrgAuth;
+2
View File
@@ -4,6 +4,8 @@ import { body, param } from 'express-validator';
import { requireAuth, validateRequest } from '../../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { membershipController } from '../../controllers/v1'; import { membershipController } from '../../controllers/v1';
// note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId)
router.get( // used for old CLI (deprecate) router.get( // used for old CLI (deprecate)
'/:workspaceId/connect', '/:workspaceId/connect',
requireAuth({ requireAuth({
+3 -3
View File
@@ -9,7 +9,7 @@ import {
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables'; import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables';
import { organizationController } from '../../controllers/v1'; import { organizationController } from '../../controllers/v1';
router.get( router.get( // deprecated (moved to api/v2/users/me/organizations)
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt']
@@ -41,7 +41,7 @@ router.get(
organizationController.getOrganization organizationController.getOrganization
); );
router.get( router.get( // deprecated (moved to api/v2/organizations/:organizationId/memberships)
'/:organizationId/users', '/:organizationId/users',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt']
@@ -56,7 +56,7 @@ router.get(
); );
router.get( router.get(
'/:organizationId/my-workspaces', '/:organizationId/my-workspaces', // deprecated (moved to api/v2/organizations/:organizationId/workspaces)
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt']
}), }),
+5 -3
View File
@@ -1,16 +1,18 @@
import users from './users'; import users from './users';
import secret from './secret'; // stop-supporting import organizations from './organizations';
import secrets from './secrets';
import workspace from './workspace'; import workspace from './workspace';
import secret from './secret'; // deprecated
import secrets from './secrets';
import serviceTokenData from './serviceTokenData'; import serviceTokenData from './serviceTokenData';
import apiKeyData from './apiKeyData'; import apiKeyData from './apiKeyData';
import environment from "./environment" import environment from "./environment"
export { export {
users, users,
organizations,
workspace,
secret, secret,
secrets, secrets,
workspace,
serviceTokenData, serviceTokenData,
apiKeyData, apiKeyData,
environment environment
+80
View File
@@ -0,0 +1,80 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
requireOrganizationAuth,
requireMembershipOrgAuth,
validateRequest
} from '../../middleware';
import { body, param, query } from 'express-validator';
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables';
import { organizationsController } from '../../controllers/v2';
// TODO: /POST to create membership
router.get(
'/:organizationId/memberships',
param('organizationId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
organizationsController.getOrganizationMemberships
);
router.patch(
'/:organizationId/memberships/:membershipId',
param('organizationId').exists().trim(),
param('membershipId').exists().trim(),
body('role').exists().isString().trim().isIn([ADMIN, MEMBER]),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
requireMembershipOrgAuth({
acceptedRoles: [OWNER, ADMIN]
}),
organizationsController.updateOrganizationMembership
);
router.delete(
'/:organizationId/memberships/:membershipId',
param('organizationId').exists().trim(),
param('membershipId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
requireMembershipOrgAuth({
acceptedRoles: [OWNER, ADMIN]
}),
organizationsController.deleteOrganizationMembership
);
router.get(
'/:organizationId/workspaces',
param('organizationId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
organizationsController.getOrganizationWorkspaces
);
export default router;
+1 -1
View File
@@ -10,7 +10,7 @@ import { ADMIN, MEMBER } from '../../variables';
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret'; import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
import { secretController } from '../../controllers/v2'; import { secretController } from '../../controllers/v2';
// note to devs: stop supporting // note to devs: stop supporting these routes [deprecated]
const router = express.Router(); const router = express.Router();
+8
View File
@@ -13,4 +13,12 @@ router.get(
usersController.getMe usersController.getMe
); );
router.get(
'/me/organizations',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
usersController.getMyOrganizations
);
export default router; export default router;
+17 -17
View File
@@ -83,23 +83,6 @@ router.get( // new - TODO: rewire dashboard to this route
workspaceController.getWorkspaceMemberships workspaceController.getWorkspaceMemberships
); );
router.delete( // TODO - rewire dashboard to this route
'/:workspaceId/memberships/:membershipId',
param('workspaceId').exists().trim(),
param('membershipId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
}),
requireMembershipAuth({
acceptedRoles: [ADMIN]
}),
workspaceController.deleteWorkspaceMembership
);
router.patch( // TODO - rewire dashboard to this route router.patch( // TODO - rewire dashboard to this route
'/:workspaceId/memberships/:membershipId', '/:workspaceId/memberships/:membershipId',
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
@@ -118,4 +101,21 @@ router.patch( // TODO - rewire dashboard to this route
workspaceController.updateWorkspaceMembership workspaceController.updateWorkspaceMembership
); );
router.delete( // TODO - rewire dashboard to this route
'/:workspaceId/memberships/:membershipId',
param('workspaceId').exists().trim(),
param('membershipId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
}),
requireMembershipAuth({
acceptedRoles: [ADMIN]
}),
workspaceController.deleteWorkspaceMembership
);
export default router; export default router;
+27
View File
@@ -66,6 +66,33 @@ const generateOpenAPISpec = async () => {
workspace: '', workspace: '',
role: 'admin' role: 'admin'
}, },
MembershipOrg: {
user: {
_id: '',
email: '',
firstName: '',
lastName: '',
publicKey: '',
updatedAt: '',
createdAt: ''
},
organization: '',
role: 'owner',
status: 'accepted'
},
Organization: {
_id: '',
name: '',
customerId: ''
},
Project: {
name: 'My Project',
organization: '',
environments: [{
name: 'development',
slug: 'dev'
}]
},
ProjectKey: { ProjectKey: {
encryptedkey: '', encryptedkey: '',
nonce: '', nonce: '',
@@ -0,0 +1,4 @@
---
title: "Delete Membership"
openapi: "DELETE /api/v2/organizations/{organizationId}/memberships/{membershipId}"
---
@@ -0,0 +1,4 @@
---
title: "Get Memberships"
openapi: "GET /api/v2/organizations/{organizationId}/memberships"
---
@@ -0,0 +1,4 @@
---
title: "Update Membership"
openapi: "PATCH /api/v2/organizations/{organizationId}/memberships/{membershipId}"
---
@@ -0,0 +1,4 @@
---
title: "Get Projects"
openapi: "GET /api/v2/organizations/{organizationId}/workspaces"
---
+1 -1
View File
@@ -1,4 +1,4 @@
--- ---
title: "Get Current User" title: "Get My User"
openapi: "GET /api/v2/users/me" openapi: "GET /api/v2/users/me"
--- ---
@@ -0,0 +1,4 @@
---
title: "Get My Organizations"
openapi: "GET /api/v2/users/me/organizations"
---
+11 -1
View File
@@ -162,7 +162,17 @@
{ {
"group": "Users", "group": "Users",
"pages": [ "pages": [
"api-reference/endpoints/users/me" "api-reference/endpoints/users/me",
"api-reference/endpoints/users/my-organizations"
]
},
{
"group": "Organizations",
"pages": [
"api-reference/endpoints/organizations/memberships",
"api-reference/endpoints/organizations/update-membership",
"api-reference/endpoints/organizations/delete-membership",
"api-reference/endpoints/organizations/workspaces"
] ]
}, },
{ {
+325 -34
View File
@@ -1392,6 +1392,229 @@ paths:
description: Bad Request description: Bad Request
security: security:
- apiKeyAuth: [] - apiKeyAuth: []
/api/v2/users/me/organizations:
get:
summary: Return organizations that current user is part of
description: Return organizations that current user is part of
parameters: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
organizations:
type: array
items:
$ref: '#/components/schemas/Organization'
description: Organizations that user is part of
'400':
description: Bad Request
security:
- apiKeyAuth: []
/api/v2/organizations/{organizationId}/memberships:
get:
summary: Return organization memberships
description: Return organization memberships
parameters:
- name: organizationId
in: path
required: true
schema:
type: string
description: ID of organization
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
memberships:
type: array
items:
$ref: '#/components/schemas/MembershipOrg'
description: Memberships of organization
'400':
description: Bad Request
security:
- apiKeyAuth: []
/api/v2/organizations/{organizationId}/memberships/{membershipId}:
patch:
summary: Update organization membership
description: Update organization membership
parameters:
- name: organizationId
in: path
required: true
schema:
type: string
description: ID of organization
- name: membershipId
in: path
required: true
schema:
type: string
description: ID of organization membership to update
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
membership:
$ref: '#/components/schemas/MembershipOrg'
description: Updated organization membership
'400':
description: Bad Request
security:
- apiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
role:
type: string
description: >-
Role of organization membership - either owner, admin, or
member
delete:
summary: Delete organization membership
description: Delete organization membership
parameters:
- name: organizationId
in: path
required: true
schema:
type: string
description: ID of organization
- name: membershipId
in: path
required: true
schema:
type: string
description: ID of organization membership to delete
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
membership:
$ref: '#/components/schemas/MembershipOrg'
description: Deleted organization membership
'400':
description: Bad Request
security:
- apiKeyAuth: []
/api/v2/organizations/{organizationId}/workspaces:
get:
summary: Return projects in organization that user is part of
description: Return projects in organization that user is part of
parameters:
- name: organizationId
in: path
required: true
schema:
type: string
description: ID of organization
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
workspaces:
type: array
items:
$ref: '#/components/schemas/Project'
description: Projects of organization
'400':
description: Bad Request
security:
- apiKeyAuth: []
/api/v2/workspace/{workspaceId}/environments:
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:
environmentName:
example: any
environmentSlug:
example: any
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:
- 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:
environmentSlug:
example: any
/api/v2/workspace/{workspaceId}/secrets: /api/v2/workspace/{workspaceId}/secrets:
post: post:
description: '' description: ''
@@ -1509,37 +1732,6 @@ paths:
security: security:
- apiKeyAuth: [] - apiKeyAuth: []
/api/v2/workspace/{workspaceId}/memberships/{membershipId}: /api/v2/workspace/{workspaceId}/memberships/{membershipId}:
delete:
summary: Delete project membership
description: Delete project membership
parameters:
- name: workspaceId
in: path
required: true
schema:
type: string
description: ID of project
- name: membershipId
in: path
required: true
schema:
type: string
description: ID of membership
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
membership:
$ref: '#/components/schemas/Membership'
description: Deleted membership
'400':
description: Bad Request
security:
- apiKeyAuth: []
patch: patch:
summary: Update project membership summary: Update project membership
description: Update project membership description: Update project membership
@@ -1555,7 +1747,7 @@ paths:
required: true required: true
schema: schema:
type: string type: string
description: ID of membership description: ID of project membership to update
responses: responses:
'200': '200':
description: OK description: OK
@@ -1581,6 +1773,37 @@ paths:
role: role:
type: string type: string
description: Role of membership - either admin or member description: Role of membership - either admin or member
delete:
summary: Delete project membership
description: Delete project membership
parameters:
- name: workspaceId
in: path
required: true
schema:
type: string
description: ID of project
- name: membershipId
in: path
required: true
schema:
type: string
description: ID of project membership to delete
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
membership:
$ref: '#/components/schemas/Membership'
description: Deleted membership
'400':
description: Bad Request
security:
- apiKeyAuth: []
/api/v2/secret/batch-create/workspace/{workspaceId}/environment/{environment}: /api/v2/secret/batch-create/workspace/{workspaceId}/environment/{environment}:
post: post:
description: '' description: ''
@@ -1927,7 +2150,7 @@ paths:
description: OK description: OK
'400': '400':
description: Bad Request description: Bad Request
/api/v2/api-key-data/: /api/v2/api-key/:
get: get:
description: '' description: ''
parameters: [] parameters: []
@@ -1954,7 +2177,7 @@ paths:
example: any example: any
expiresIn: expiresIn:
example: any example: any
/api/v2/api-key-data/{apiKeyDataId}: /api/v2/api-key/{apiKeyDataId}:
delete: delete:
description: '' description: ''
parameters: parameters:
@@ -2043,6 +2266,74 @@ components:
role: role:
type: string type: string
example: admin example: admin
MembershipOrg:
type: object
properties:
user:
type: object
properties:
_id:
type: string
example: ''
email:
type: string
example: ''
firstName:
type: string
example: ''
lastName:
type: string
example: ''
publicKey:
type: string
example: ''
updatedAt:
type: string
example: ''
createdAt:
type: string
example: ''
organization:
type: string
example: ''
role:
type: string
example: owner
status:
type: string
example: accepted
Organization:
type: object
properties:
_id:
type: string
example: ''
name:
type: string
example: ''
customerId:
type: string
example: ''
Project:
type: object
properties:
name:
type: string
example: My Project
organization:
type: string
example: ''
environments:
type: array
items:
type: object
properties:
name:
type: string
example: development
slug:
type: string
example: dev
ProjectKey: ProjectKey:
type: object type: object
properties: properties: