mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #491 from Infisical/railway
Railway Integration + Service Accounts
This commit is contained in:
@@ -13,7 +13,8 @@ import {
|
||||
revokeAccess
|
||||
} from '../../integrations';
|
||||
import {
|
||||
INTEGRATION_VERCEL_API_URL
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_RAILWAY_API_URL
|
||||
} from '../../variables';
|
||||
import request from '../../config/request';
|
||||
|
||||
@@ -203,7 +204,8 @@ export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of available Vercel (preview) branches
|
||||
* Return list of available Vercel (preview) branches for Vercel project with
|
||||
* id [appId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
@@ -246,6 +248,151 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of Railway environments for Railway project with
|
||||
* id [appId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: Response) => {
|
||||
const { integrationAuthId } = req.params;
|
||||
const appId = req.query.appId as string;
|
||||
|
||||
interface RailwayEnvironment {
|
||||
node: {
|
||||
id: string;
|
||||
name: string;
|
||||
isEphemeral: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
interface Environment {
|
||||
environmentId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
let environments: Environment[] = [];
|
||||
|
||||
if (appId && appId !== '') {
|
||||
const query = `
|
||||
query GetEnvironments($projectId: String!, $after: String, $before: String, $first: Int, $isEphemeral: Boolean, $last: Int) {
|
||||
environments(projectId: $projectId, after: $after, before: $before, first: $first, isEphemeral: $isEphemeral, last: $last) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
isEphemeral
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const variables = {
|
||||
projectId: appId
|
||||
}
|
||||
|
||||
const { data: { data: { environments: { edges } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
|
||||
query,
|
||||
variables,
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${req.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
environments = edges.map((e: RailwayEnvironment) => {
|
||||
return ({
|
||||
name: e.node.name,
|
||||
environmentId: e.node.id
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
environments
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of Railway services for Railway project with id
|
||||
* [appId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getIntegrationAuthRailwayServices = async (req: Request, res: Response) => {
|
||||
const { integrationAuthId } = req.params;
|
||||
const appId = req.query.appId as string;
|
||||
|
||||
interface RailwayService {
|
||||
node: {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface Service {
|
||||
name: string;
|
||||
serviceId: string;
|
||||
}
|
||||
|
||||
let services: Service[] = [];
|
||||
|
||||
const query = `
|
||||
query project($id: String!) {
|
||||
project(id: $id) {
|
||||
createdAt
|
||||
deletedAt
|
||||
id
|
||||
description
|
||||
expiredAt
|
||||
isPublic
|
||||
isTempProject
|
||||
isUpdatable
|
||||
name
|
||||
prDeploys
|
||||
teamId
|
||||
updatedAt
|
||||
upstreamUrl
|
||||
services {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
if (appId && appId !== '') {
|
||||
const variables = {
|
||||
id: appId
|
||||
}
|
||||
|
||||
const { data: { data: { project: { services: { edges } } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
|
||||
query,
|
||||
variables
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${req.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
services = edges.map((e: RailwayService) => ({
|
||||
name: e.node.name,
|
||||
serviceId: e.node.id
|
||||
}));
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
services
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete integration authorization with id [integrationAuthId]
|
||||
* @param req
|
||||
|
||||
@@ -24,6 +24,9 @@ export const createIntegration = async (req: Request, res: Response) => {
|
||||
isActive,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region
|
||||
@@ -39,12 +42,15 @@ export const createIntegration = async (req: Request, res: Response) => {
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
integration: req.integrationAuth.integration,
|
||||
integrationAuth: new Types.ObjectId(integrationAuthId)
|
||||
}).save();
|
||||
}).save();
|
||||
|
||||
if (integration) {
|
||||
// trigger event - push secrets
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { pushKeys } from '../../helpers/key';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
import { EventService } from '../../services';
|
||||
import { getPostHogClient } from '../../services';
|
||||
import { TelemetryService } from '../../services';
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
@@ -38,7 +38,7 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
|
||||
try {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
let { secrets }: { secrets: PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
@@ -112,7 +112,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
let key;
|
||||
try {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
@@ -181,7 +181,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
let key;
|
||||
try {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
@@ -50,6 +50,7 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
|
||||
|
||||
apiKeyData = await new APIKeyData({
|
||||
name,
|
||||
lastUsed: new Date(),
|
||||
expiresAt,
|
||||
user: req.user._id,
|
||||
secretHash
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { SecretVersion } from '../../ee/models';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
import _ from 'lodash';
|
||||
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
|
||||
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
|
||||
|
||||
/**
|
||||
* Create new workspace environment named [environmentName] under workspace with id
|
||||
@@ -244,8 +244,8 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (
|
||||
throw BadRequestError()
|
||||
}
|
||||
relatedWorkspace.environments.forEach(environment => {
|
||||
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ })
|
||||
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE })
|
||||
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS })
|
||||
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS })
|
||||
if (isReadBlocked && isWriteBlocked) {
|
||||
return
|
||||
} else {
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as serviceTokenDataController from './serviceTokenDataController';
|
||||
import * as apiKeyDataController from './apiKeyDataController';
|
||||
import * as secretController from './secretController';
|
||||
import * as secretsController from './secretsController';
|
||||
import * as serviceAccountsController from './serviceAccountsController';
|
||||
import * as environmentController from './environmentController';
|
||||
import * as tagController from './tagController';
|
||||
|
||||
@@ -20,6 +21,7 @@ export {
|
||||
apiKeyDataController,
|
||||
secretController,
|
||||
secretsController,
|
||||
serviceAccountsController,
|
||||
environmentController,
|
||||
tagController
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
MembershipOrg,
|
||||
Membership,
|
||||
Workspace
|
||||
Workspace,
|
||||
ServiceAccount
|
||||
} from '../../models';
|
||||
import { deleteMembershipOrg } from '../../helpers/membershipOrg';
|
||||
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
||||
@@ -260,37 +262,45 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
|
||||
}
|
||||
}
|
||||
*/
|
||||
let workspaces;
|
||||
try {
|
||||
const { organizationId } = req.params;
|
||||
const { organizationId } = req.params;
|
||||
|
||||
const workspacesSet = new Set(
|
||||
(
|
||||
await Workspace.find(
|
||||
{
|
||||
organization: organizationId
|
||||
},
|
||||
'_id'
|
||||
)
|
||||
).map((w) => w._id.toString())
|
||||
);
|
||||
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({
|
||||
const workspaces = (
|
||||
await Membership.find({
|
||||
user: req.user._id
|
||||
}).populate('workspace')
|
||||
)
|
||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||
.map((m) => m.workspace);
|
||||
|
||||
return res.status(200).send({
|
||||
workspaces
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return service accounts for organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getOrganizationServiceAccounts = async (req: Request, res: Response) => {
|
||||
const { organizationId } = req.params;
|
||||
|
||||
const serviceAccounts = await ServiceAccount.find({
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccounts
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,7 @@ const { ValidationError } = mongoose.Error;
|
||||
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
|
||||
import { AnyBulkWriteOperation } from 'mongodb';
|
||||
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
|
||||
import { getPostHogClient } from '../../services';
|
||||
import { TelemetryService } from '../../services';
|
||||
|
||||
/**
|
||||
* Create secret for workspace with id [workspaceId] and environment [environment]
|
||||
@@ -15,7 +15,7 @@ import { getPostHogClient } from '../../services';
|
||||
* @param res
|
||||
*/
|
||||
export const createSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const secretToCreate: CreateSecretRequestBody = req.body.secret;
|
||||
const { workspaceId, environment } = req.params
|
||||
const sanitizedSecret: SanitizedSecretForCreate = {
|
||||
@@ -68,7 +68,7 @@ export const createSecret = async (req: Request, res: Response) => {
|
||||
* @param res
|
||||
*/
|
||||
export const createSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
|
||||
const { workspaceId, environment } = req.params
|
||||
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
|
||||
@@ -130,7 +130,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const { workspaceId, environmentName } = req.params
|
||||
const secretIdsToDelete: string[] = req.body.secretIds
|
||||
|
||||
@@ -184,7 +184,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
await Secret.findByIdAndDelete(req._secret._id)
|
||||
|
||||
if (postHogClient) {
|
||||
@@ -213,7 +213,7 @@ export const deleteSecret = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const updateSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const { workspaceId, environmentName } = req.params
|
||||
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
|
||||
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
|
||||
@@ -281,7 +281,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const updateSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const { workspaceId, environmentName } = req.params
|
||||
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
|
||||
|
||||
@@ -335,7 +335,7 @@ export const updateSecret = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const getSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const { environment } = req.query;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@ import { UnauthorizedRequestError, ValidationError } from '../../utils/errors';
|
||||
import { EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
import { EESecretService, EELogService } from '../../ee/services';
|
||||
import { getPostHogClient } from '../../services';
|
||||
import { TelemetryService } from '../../services';
|
||||
import { getChannelFromUserAgent } from '../../utils/posthog';
|
||||
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
|
||||
import { PERMISSION_WRITE_SECRETS } from '../../variables';
|
||||
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
|
||||
import Tag from '../../models/tag';
|
||||
import _ from 'lodash';
|
||||
import _, { eq } from 'lodash';
|
||||
import {
|
||||
BatchSecretRequest,
|
||||
BatchSecret
|
||||
@@ -28,12 +28,13 @@ import {
|
||||
|
||||
/**
|
||||
* Peform a batch of any specified CUD secret operations
|
||||
* (used by dashboard)
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const batchSecrets = async (req: Request, res: Response) => {
|
||||
const channel = getChannelFromUserAgent(req.headers['user-agent']);
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
|
||||
const {
|
||||
workspaceId,
|
||||
@@ -91,7 +92,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
|
||||
const addAction = await EELogService.createAction({
|
||||
name: ACTION_ADD_SECRETS,
|
||||
userId: req.user._id,
|
||||
userId: req.user?._id,
|
||||
serviceAccountId: req.serviceAccount?._id,
|
||||
serviceTokenDataId: req.serviceTokenData?._id,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secretIds: createdSecrets.map((n) => n._id)
|
||||
}) as IAction;
|
||||
@@ -328,14 +331,15 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
*/
|
||||
const postHogClient = getPostHogClient();
|
||||
|
||||
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
||||
const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body;
|
||||
|
||||
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_WRITE)
|
||||
if (!hasAccess) {
|
||||
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
||||
if (req.user) {
|
||||
const hasAccess = await userHasWorkspaceAccess(req.user, new Types.ObjectId(workspaceId), environment, PERMISSION_WRITE_SECRETS)
|
||||
if (!hasAccess) {
|
||||
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
||||
}
|
||||
}
|
||||
|
||||
let listOfSecretsToCreate;
|
||||
@@ -378,7 +382,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
version: 1,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
type,
|
||||
user: type === SECRET_PERSONAL ? req.user : undefined,
|
||||
user: (req.user && type === SECRET_PERSONAL) ? req.user : undefined,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
@@ -391,7 +395,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
secretCommentTag,
|
||||
tags
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject());
|
||||
|
||||
@@ -447,14 +451,18 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
|
||||
const addAction = await EELogService.createAction({
|
||||
name: ACTION_ADD_SECRETS,
|
||||
userId: req.user._id,
|
||||
userId: req.user?._id,
|
||||
serviceAccountId: req.serviceAccount?._id,
|
||||
serviceTokenDataId: req.serviceTokenData?._id,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secretIds: newlyCreatedSecrets.map((n) => n._id)
|
||||
});
|
||||
|
||||
// (EE) create (audit) log
|
||||
addAction && await EELogService.createLog({
|
||||
userId: req.user._id.toString(),
|
||||
userId: req.user?._id,
|
||||
serviceAccountId: req.serviceAccount?._id,
|
||||
serviceTokenDataId: req.serviceTokenData?._id,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
actions: [addAction],
|
||||
channel,
|
||||
@@ -466,10 +474,15 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId
|
||||
});
|
||||
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets added',
|
||||
distinctId: req.user.email,
|
||||
distinctId: TelemetryService.getDistinctId({
|
||||
user: req.user,
|
||||
serviceAccount: req.serviceAccount,
|
||||
serviceTokenData: req.serviceTokenData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: listOfSecretsToCreate.length,
|
||||
environment,
|
||||
@@ -533,91 +546,118 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
}
|
||||
*/
|
||||
|
||||
const postHogClient = getPostHogClient();
|
||||
const { tagSlugs } = req.query;
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
|
||||
const { workspaceId, environment, tagSlugs } = req.query;
|
||||
// tags logic
|
||||
let tagIds = [];
|
||||
const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : [];
|
||||
let userId = "" // used for getting personal secrets for user
|
||||
let userEmail = "" // used for posthog
|
||||
if (req.user) {
|
||||
userId = req.user._id;
|
||||
userEmail = req.user.email;
|
||||
}
|
||||
|
||||
if (req.serviceTokenData) {
|
||||
userId = req.serviceTokenData.user._id
|
||||
userEmail = req.serviceTokenData.user.email;
|
||||
}
|
||||
|
||||
// none service token case as service tokens are already scoped to env and project
|
||||
let hasWriteOnlyAccess
|
||||
if (!req.serviceTokenData) {
|
||||
hasWriteOnlyAccess = await userHasWriteOnlyAbility(userId, workspaceId, environment)
|
||||
const hasNoAccess = await userHasNoAbility(userId, workspaceId, environment)
|
||||
if (hasNoAccess) {
|
||||
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
||||
}
|
||||
}
|
||||
let secrets: any
|
||||
let secretQuery: any
|
||||
|
||||
if (tagNamesList != undefined && tagNamesList.length != 0) {
|
||||
const workspaceFromDB = await Tag.find({ workspace: workspaceId })
|
||||
|
||||
const tagIds = _.map(tagNamesList, (tagName) => {
|
||||
const workspaceFromDB = await Tag.find({ workspace: workspaceId });
|
||||
tagIds = _.map(tagNamesList, (tagName) => {
|
||||
const tag = _.find(workspaceFromDB, { slug: tagName });
|
||||
return tag ? tag.id : null;
|
||||
});
|
||||
}
|
||||
|
||||
let secrets: ISecret[] = [];
|
||||
|
||||
if (req.user) {
|
||||
// case: client authorization is via JWT
|
||||
|
||||
secretQuery = {
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
$or: [
|
||||
{ user: userId },
|
||||
{ user: { $exists: false } }
|
||||
],
|
||||
tags: { $in: tagIds },
|
||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||
let hasWriteOnlyAccess
|
||||
if (!req.serviceTokenData) {
|
||||
hasWriteOnlyAccess = await userHasWriteOnlyAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
|
||||
const hasNoAccess = await userHasNoAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
|
||||
if (hasNoAccess) {
|
||||
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
secretQuery = {
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
$or: [
|
||||
{ user: userId },
|
||||
{ user: { $exists: false } }
|
||||
],
|
||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||
|
||||
let secretQuery: any;
|
||||
if (tagNamesList != undefined && tagNamesList.length != 0) {
|
||||
const workspaceFromDB = await Tag.find({ workspace: workspaceId })
|
||||
|
||||
const tagIds = _.map(tagNamesList, (tagName) => {
|
||||
const tag = _.find(workspaceFromDB, { slug: tagName });
|
||||
return tag ? tag.id : null;
|
||||
});
|
||||
|
||||
secretQuery = {
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
$or: [
|
||||
{ user: req.user._id },
|
||||
{ user: { $exists: false } }
|
||||
],
|
||||
tags: { $in: tagIds },
|
||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||
}
|
||||
} else {
|
||||
secretQuery = {
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
$or: [
|
||||
{ user: req.user._id },
|
||||
{ user: { $exists: false } }
|
||||
],
|
||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWriteOnlyAccess) {
|
||||
// (i.e. you don't get values to decrypt since you can only write)
|
||||
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag")
|
||||
} else {
|
||||
secrets = await Secret.find(secretQuery).populate("tags")
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWriteOnlyAccess) {
|
||||
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag")
|
||||
} else {
|
||||
secrets = await Secret.find(secretQuery).populate("tags")
|
||||
|
||||
if (req.serviceAccount || req.serviceTokenData) {
|
||||
// case: client authorization is either via service account or service token
|
||||
|
||||
secrets = await Secret.find({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
user: {
|
||||
$exists: false
|
||||
},
|
||||
...(tagIds.length > 0 ? { tags: { $in: tagIds } } : {}),
|
||||
type: SECRET_SHARED
|
||||
});
|
||||
}
|
||||
|
||||
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
||||
|
||||
const readAction = await EELogService.createAction({
|
||||
name: ACTION_READ_SECRETS,
|
||||
userId: new Types.ObjectId(userId),
|
||||
userId: req.user?._id,
|
||||
serviceAccountId: req.serviceAccount?._id,
|
||||
serviceTokenDataId: req.serviceTokenData?._id,
|
||||
workspaceId: new Types.ObjectId(workspaceId as string),
|
||||
secretIds: secrets.map((n: any) => n._id)
|
||||
});
|
||||
|
||||
|
||||
readAction && await EELogService.createLog({
|
||||
userId: new Types.ObjectId(userId),
|
||||
userId: req.user?._id,
|
||||
serviceAccountId: req.serviceAccount?._id,
|
||||
serviceTokenDataId: req.serviceTokenData?._id,
|
||||
workspaceId: new Types.ObjectId(workspaceId as string),
|
||||
actions: [readAction],
|
||||
channel,
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets pulled',
|
||||
distinctId: userEmail,
|
||||
distinctId: TelemetryService.getDistinctId({
|
||||
user: req.user,
|
||||
serviceAccount: req.serviceAccount,
|
||||
serviceTokenData: req.serviceTokenData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
@@ -633,59 +673,6 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export const getOnlySecretKeys = async (req: Request, res: Response) => {
|
||||
const { workspaceId, environment } = req.query;
|
||||
|
||||
let userId = "" // used for getting personal secrets for user
|
||||
let userEmail = "" // used for posthog
|
||||
if (req.user) {
|
||||
userId = req.user._id;
|
||||
userEmail = req.user.email;
|
||||
}
|
||||
|
||||
if (req.serviceTokenData) {
|
||||
userId = req.serviceTokenData.user._id
|
||||
userEmail = req.serviceTokenData.user.email;
|
||||
}
|
||||
|
||||
// none service token case as service tokens are already scoped
|
||||
if (!req.serviceTokenData) {
|
||||
const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ)
|
||||
if (!hasAccess) {
|
||||
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
||||
}
|
||||
}
|
||||
|
||||
const [err, secretKeys] = await to(Secret.find(
|
||||
{
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
$or: [
|
||||
{ user: userId },
|
||||
{ user: { $exists: false } }
|
||||
],
|
||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||
}
|
||||
)
|
||||
.select("secretKeyIV secretKeyTag secretKeyCiphertext")
|
||||
.then())
|
||||
|
||||
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
|
||||
|
||||
// readAction && await EELogService.createLog({
|
||||
// userId: new Types.ObjectId(userId),
|
||||
// workspaceId: new Types.ObjectId(workspaceId as string),
|
||||
// actions: [readAction],
|
||||
// channel,
|
||||
// ipAddress: req.ip
|
||||
// });
|
||||
|
||||
return res.status(200).send({
|
||||
secretKeys
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update secret(s)
|
||||
* @param req
|
||||
@@ -736,10 +723,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
*/
|
||||
const postHogClient = getPostHogClient();
|
||||
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||
|
||||
// TODO: move type
|
||||
interface PatchSecret {
|
||||
id: string;
|
||||
secretKeyCiphertext: string;
|
||||
@@ -865,14 +850,18 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
|
||||
const updateAction = await EELogService.createAction({
|
||||
name: ACTION_UPDATE_SECRETS,
|
||||
userId: req.user._id,
|
||||
userId: req.user?._id,
|
||||
serviceAccountId: req.serviceAccount?._id,
|
||||
serviceTokenDataId: req.serviceTokenData?._id,
|
||||
workspaceId: new Types.ObjectId(key),
|
||||
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
|
||||
});
|
||||
|
||||
// (EE) create (audit) log
|
||||
updateAction && await EELogService.createLog({
|
||||
userId: req.user._id.toString(),
|
||||
userId: req.user?._id,
|
||||
serviceAccountId: req.serviceAccount?._id,
|
||||
serviceTokenDataId: req.serviceTokenData?._id,
|
||||
workspaceId: new Types.ObjectId(key),
|
||||
actions: [updateAction],
|
||||
channel,
|
||||
@@ -884,10 +873,15 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId: key
|
||||
})
|
||||
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets modified',
|
||||
distinctId: req.user.email,
|
||||
distinctId: TelemetryService.getDistinctId({
|
||||
user: req.user,
|
||||
serviceAccount: req.serviceAccount,
|
||||
serviceTokenData: req.serviceTokenData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: workspaceSecretObj[key].length,
|
||||
environment: workspaceSecretObj[key][0].environment,
|
||||
@@ -909,7 +903,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete secret(s) with id [workspaceId] and environment [environment]
|
||||
* Delete secret(s)
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
@@ -958,7 +952,11 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
*/
|
||||
const postHogClient = getPostHogClient();
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'delete secrets!!'
|
||||
});
|
||||
|
||||
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
||||
const toDelete = req.secrets.map((s: any) => s._id);
|
||||
|
||||
@@ -992,14 +990,18 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
});
|
||||
const deleteAction = await EELogService.createAction({
|
||||
name: ACTION_DELETE_SECRETS,
|
||||
userId: req.user._id,
|
||||
userId: req.user?._id,
|
||||
serviceAccountId: req.serviceAccount?._id,
|
||||
serviceTokenDataId: req.serviceTokenData?._id,
|
||||
workspaceId: new Types.ObjectId(key),
|
||||
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
|
||||
});
|
||||
|
||||
// (EE) create (audit) log
|
||||
deleteAction && await EELogService.createLog({
|
||||
userId: req.user._id.toString(),
|
||||
userId: req.user?._id,
|
||||
serviceAccountId: req.serviceAccount?._id,
|
||||
serviceTokenDataId: req.serviceTokenData?._id,
|
||||
workspaceId: new Types.ObjectId(key),
|
||||
actions: [deleteAction],
|
||||
channel,
|
||||
@@ -1011,10 +1013,15 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId: key
|
||||
})
|
||||
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets deleted',
|
||||
distinctId: req.user.email,
|
||||
distinctId: TelemetryService.getDistinctId({
|
||||
user: req.user,
|
||||
serviceAccount: req.serviceAccount,
|
||||
serviceTokenData: req.serviceTokenData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: workspaceSecretObj[key].length,
|
||||
environment: workspaceSecretObj[key][0].environment,
|
||||
|
||||
306
backend/src/controllers/v2/serviceAccountsController.ts
Normal file
306
backend/src/controllers/v2/serviceAccountsController.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import {
|
||||
ServiceAccount,
|
||||
ServiceAccountKey,
|
||||
ServiceAccountOrganizationPermission,
|
||||
ServiceAccountWorkspacePermission
|
||||
} from '../../models';
|
||||
import {
|
||||
CreateServiceAccountDto
|
||||
} from '../../interfaces/serviceAccounts/dto';
|
||||
import { BadRequestError, ServiceAccountNotFoundError } from '../../utils/errors';
|
||||
import { getSaltRounds } from '../../config';
|
||||
|
||||
/**
|
||||
* Return service account tied to the request (service account) client
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getCurrentServiceAccount = async (req: Request, res: Response) => {
|
||||
const serviceAccount = await ServiceAccount.findById(req.serviceAccount._id);
|
||||
|
||||
if (!serviceAccount) {
|
||||
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccount
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getServiceAccountById = async (req: Request, res: Response) => {
|
||||
const { serviceAccountId } = req.params;
|
||||
|
||||
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
|
||||
|
||||
if (!serviceAccount) {
|
||||
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccount
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new service account under organization with id [organizationId]
|
||||
* that has access to workspaces [workspaces]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createServiceAccount = async (req: Request, res: Response) => {
|
||||
const {
|
||||
name,
|
||||
organizationId,
|
||||
publicKey,
|
||||
expiresIn,
|
||||
}: CreateServiceAccountDto = req.body;
|
||||
|
||||
let expiresAt;
|
||||
if (expiresIn) {
|
||||
expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
}
|
||||
|
||||
const secret = crypto.randomBytes(16).toString('base64');
|
||||
const secretHash = await bcrypt.hash(secret, getSaltRounds());
|
||||
|
||||
// create service account
|
||||
const serviceAccount = await new ServiceAccount({
|
||||
name,
|
||||
organization: new Types.ObjectId(organizationId),
|
||||
user: req.user,
|
||||
publicKey,
|
||||
lastUsed: new Date(),
|
||||
expiresAt,
|
||||
secretHash
|
||||
}).save();
|
||||
|
||||
const serviceAccountObj = serviceAccount.toObject();
|
||||
|
||||
delete serviceAccountObj.secretHash;
|
||||
|
||||
// provision default org-level permission for service account
|
||||
await new ServiceAccountOrganizationPermission({
|
||||
serviceAccount: serviceAccount._id
|
||||
}).save();
|
||||
|
||||
const secretId = Buffer.from(serviceAccount._id.toString(), 'hex').toString('base64');
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccountAccessKey: `sa.${secretId}.${secret}`,
|
||||
serviceAccount: serviceAccountObj
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change name of service account with id [serviceAccountId] to [name]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const changeServiceAccountName = async (req: Request, res: Response) => {
|
||||
const { serviceAccountId } = req.params;
|
||||
const { name } = req.body;
|
||||
|
||||
const serviceAccount = await ServiceAccount.findOneAndUpdate(
|
||||
{
|
||||
_id: new Types.ObjectId(serviceAccountId)
|
||||
},
|
||||
{
|
||||
name
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccount
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a service account key to service account with id [serviceAccountId]
|
||||
* for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const addServiceAccountKey = async (req: Request, res: Response) => {
|
||||
const {
|
||||
workspaceId,
|
||||
encryptedKey,
|
||||
nonce
|
||||
} = req.body;
|
||||
|
||||
const serviceAccountKey = await new ServiceAccountKey({
|
||||
encryptedKey,
|
||||
nonce,
|
||||
sender: req.user._id,
|
||||
serviceAccount: req.serviceAccount._d,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
}).save();
|
||||
|
||||
return serviceAccountKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return workspace-level permission for service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getServiceAccountWorkspacePermissions = async (req: Request, res: Response) => {
|
||||
const serviceAccountWorkspacePermissions = await ServiceAccountWorkspacePermission.find({
|
||||
serviceAccount: req.serviceAccount._id
|
||||
}).populate('workspace');
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccountWorkspacePermissions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a workspace permission to service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const addServiceAccountWorkspacePermission = async (req: Request, res: Response) => {
|
||||
const { serviceAccountId } = req.params;
|
||||
const {
|
||||
environment,
|
||||
workspaceId,
|
||||
read = false,
|
||||
write = false,
|
||||
encryptedKey,
|
||||
nonce
|
||||
} = req.body;
|
||||
|
||||
if (!req.membership.workspace.environments.some((e: { name: string; slug: string }) => e.slug === environment)) {
|
||||
return res.status(400).send({
|
||||
message: 'Failed to validate workspace environment'
|
||||
});
|
||||
}
|
||||
|
||||
const existingPermission = await ServiceAccountWorkspacePermission.findOne({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId),
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment
|
||||
});
|
||||
|
||||
if (existingPermission) throw BadRequestError({ message: 'Failed to add workspace permission to service account due to already-existing ' });
|
||||
|
||||
const serviceAccountWorkspacePermission = await new ServiceAccountWorkspacePermission({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId),
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
read,
|
||||
write
|
||||
}).save();
|
||||
|
||||
const existingServiceAccountKey = await ServiceAccountKey.findOne({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId),
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!existingServiceAccountKey) {
|
||||
await new ServiceAccountKey({
|
||||
encryptedKey,
|
||||
nonce,
|
||||
sender: req.user._id,
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId),
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
}).save();
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccountWorkspacePermission
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete workspace permission from service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteServiceAccountWorkspacePermission = async (req: Request, res: Response) => {
|
||||
const { serviceAccountWorkspacePermissionId } = req.params;
|
||||
const serviceAccountWorkspacePermission = await ServiceAccountWorkspacePermission.findByIdAndDelete(serviceAccountWorkspacePermissionId);
|
||||
|
||||
if (serviceAccountWorkspacePermission) {
|
||||
const { serviceAccount, workspace } = serviceAccountWorkspacePermission;
|
||||
const count = await ServiceAccountWorkspacePermission.countDocuments({
|
||||
serviceAccount,
|
||||
workspace
|
||||
});
|
||||
|
||||
if (count === 0) {
|
||||
await ServiceAccountKey.findOneAndDelete({
|
||||
serviceAccount,
|
||||
workspace
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccountWorkspacePermission
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteServiceAccount = async (req: Request, res: Response) => {
|
||||
const { serviceAccountId } = req.params;
|
||||
|
||||
const serviceAccount = await ServiceAccount.findByIdAndDelete(serviceAccountId);
|
||||
|
||||
if (serviceAccount) {
|
||||
await ServiceAccountKey.deleteMany({
|
||||
serviceAccount: serviceAccount._id
|
||||
});
|
||||
|
||||
await ServiceAccountOrganizationPermission.deleteMany({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId)
|
||||
});
|
||||
|
||||
await ServiceAccountWorkspacePermission.deleteMany({
|
||||
serviceAccount: new Types.ObjectId(serviceAccountId)
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccount
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return service account keys for service account with id [serviceAccountId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getServiceAccountKeys = async (req: Request, res: Response) => {
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
|
||||
const serviceAccountKeys = await ServiceAccountKey.find({
|
||||
serviceAccount: req.serviceAccount._id,
|
||||
...(workspaceId ? { workspace: new Types.ObjectId(workspaceId) } : {})
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccountKeys
|
||||
});
|
||||
}
|
||||
@@ -3,10 +3,16 @@ import { Request, Response } from 'express';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import {
|
||||
User,
|
||||
ServiceAccount,
|
||||
ServiceTokenData
|
||||
} from '../../models';
|
||||
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
|
||||
import { ABILITY_READ } from '../../variables/organization';
|
||||
import {
|
||||
PERMISSION_READ_SECRETS,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
} from '../../variables';
|
||||
import { getSaltRounds } from '../../config';
|
||||
|
||||
/**
|
||||
@@ -53,59 +59,57 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
let serviceToken, serviceTokenData;
|
||||
let serviceTokenData;
|
||||
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
workspaceId,
|
||||
environment,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
expiresIn,
|
||||
permissions
|
||||
} = req.body;
|
||||
const {
|
||||
name,
|
||||
workspaceId,
|
||||
environment,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
expiresIn,
|
||||
permissions
|
||||
} = req.body;
|
||||
|
||||
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ)
|
||||
if (!hasAccess) {
|
||||
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
||||
}
|
||||
const secret = crypto.randomBytes(16).toString('hex');
|
||||
const secretHash = await bcrypt.hash(secret, getSaltRounds());
|
||||
|
||||
const secret = crypto.randomBytes(16).toString('hex');
|
||||
const secretHash = await bcrypt.hash(secret, getSaltRounds());
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
|
||||
serviceTokenData = await new ServiceTokenData({
|
||||
name,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
user: req.user._id,
|
||||
expiresAt,
|
||||
secretHash,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
permissions
|
||||
}).save();
|
||||
|
||||
// return service token data without sensitive data
|
||||
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
|
||||
|
||||
if (!serviceTokenData) throw new Error('Failed to find service token data');
|
||||
|
||||
serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to create service token data'
|
||||
});
|
||||
let user, serviceAccount;
|
||||
|
||||
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
|
||||
user = req.authData.authPayload._id;
|
||||
}
|
||||
|
||||
if (req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && req.authData.authPayload instanceof ServiceAccount) {
|
||||
serviceAccount = req.authData.authPayload._id;
|
||||
}
|
||||
|
||||
serviceTokenData = await new ServiceTokenData({
|
||||
name,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
user,
|
||||
serviceAccount,
|
||||
lastUsed: new Date(),
|
||||
expiresAt,
|
||||
secretHash,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
permissions
|
||||
}).save();
|
||||
|
||||
// return service token data without sensitive data
|
||||
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
|
||||
|
||||
if (!serviceTokenData) throw new Error('Failed to find service token data');
|
||||
|
||||
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
|
||||
|
||||
return res.status(200).send({
|
||||
serviceToken,
|
||||
serviceTokenData
|
||||
@@ -119,25 +123,11 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||
let serviceTokenData;
|
||||
try {
|
||||
const { serviceTokenDataId } = req.params;
|
||||
const { serviceTokenDataId } = req.params;
|
||||
|
||||
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete service token data'
|
||||
});
|
||||
}
|
||||
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
}
|
||||
|
||||
function UnauthorizedRequestError(arg0: { message: string; }) {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
reformatPullSecrets
|
||||
} from '../../helpers/secret';
|
||||
import { pushKeys } from '../../helpers/key';
|
||||
import { getPostHogClient, EventService } from '../../services';
|
||||
import { TelemetryService, EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
|
||||
interface V2PushSecret {
|
||||
@@ -48,7 +48,7 @@ interface V2PushSecret {
|
||||
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
try {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
@@ -122,7 +122,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
export const pullSecrets = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
try {
|
||||
const postHogClient = getPostHogClient();
|
||||
const postHogClient = TelemetryService.getPostHogClient();
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
@@ -506,5 +506,4 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => {
|
||||
message: 'Successfully changed autoCapitalization setting',
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
};
|
||||
@@ -2,7 +2,8 @@ import { Request, Response } from "express";
|
||||
import { Membership, Workspace } from "../../../models";
|
||||
import { IMembershipPermission } from "../../../models/membership";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
|
||||
import { ABILITY_READ, ABILITY_WRITE, ADMIN, MEMBER } from "../../../variables/organization";
|
||||
import { ADMIN, MEMBER } from "../../../variables/organization";
|
||||
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../../variables';
|
||||
import { Builder } from "builder-pattern"
|
||||
import _ from "lodash";
|
||||
|
||||
@@ -10,7 +11,7 @@ export const denyMembershipPermissions = async (req: Request, res: Response) =>
|
||||
const { membershipId } = req.params;
|
||||
const { permissions } = req.body;
|
||||
const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => {
|
||||
if (!permission.ability || !permission.environmentSlug || ![ABILITY_READ, ABILITY_WRITE].includes(permission.ability)) {
|
||||
if (!permission.ability || !permission.environmentSlug || ![PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS].includes(permission.ability)) {
|
||||
throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" })
|
||||
}
|
||||
|
||||
|
||||
@@ -418,7 +418,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.populate('actions')
|
||||
.populate('user');
|
||||
.populate('user serviceAccount serviceTokenData');
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
|
||||
@@ -24,11 +24,15 @@ import {
|
||||
const createActionUpdateSecret = async ({
|
||||
name,
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
}: {
|
||||
name: string;
|
||||
userId: Types.ObjectId;
|
||||
userId?: Types.ObjectId;
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
workspaceId: Types.ObjectId;
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
@@ -46,6 +50,8 @@ const createActionUpdateSecret = async ({
|
||||
action = await new Action({
|
||||
name,
|
||||
user: userId,
|
||||
serviceAccount: serviceAccountId,
|
||||
serviceTokenData: serviceTokenDataId,
|
||||
workspace: workspaceId,
|
||||
payload: {
|
||||
secretVersions: latestSecretVersions
|
||||
@@ -72,11 +78,15 @@ const createActionUpdateSecret = async ({
|
||||
const createActionSecret = async ({
|
||||
name,
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
}: {
|
||||
name: string;
|
||||
userId: Types.ObjectId;
|
||||
userId?: Types.ObjectId;
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
workspaceId: Types.ObjectId;
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
@@ -94,6 +104,8 @@ const createActionSecret = async ({
|
||||
action = await new Action({
|
||||
name,
|
||||
user: userId,
|
||||
serviceAccount: serviceAccountId,
|
||||
serviceTokenData: serviceTokenDataId,
|
||||
workspace: workspaceId,
|
||||
payload: {
|
||||
secretVersions: latestSecretVersions
|
||||
@@ -110,29 +122,36 @@ const createActionSecret = async ({
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an (audit) action for user with id [userId]
|
||||
* Create an (audit) action for client with id [userId],
|
||||
* [serviceAccountId], or [serviceTokenDataId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.name - name of action
|
||||
* @param {String} obj.userId - id of user associated with action
|
||||
* @returns
|
||||
*/
|
||||
const createActionUser = ({
|
||||
const createActionClient = ({
|
||||
name,
|
||||
userId
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId
|
||||
}: {
|
||||
name: string;
|
||||
userId: Types.ObjectId;
|
||||
userId?: Types.ObjectId;
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
}) => {
|
||||
let action;
|
||||
try {
|
||||
action = new Action({
|
||||
name,
|
||||
user: userId
|
||||
user: userId,
|
||||
serviceAccount: serviceAccountId,
|
||||
serviceTokenData: serviceTokenDataId
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create user action');
|
||||
throw new Error('Failed to create client action');
|
||||
}
|
||||
|
||||
return action;
|
||||
@@ -149,11 +168,15 @@ const createActionUser = ({
|
||||
const createActionHelper = async ({
|
||||
name,
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
secretIds,
|
||||
}: {
|
||||
name: string;
|
||||
userId: Types.ObjectId;
|
||||
userId?: Types.ObjectId;
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
workspaceId?: Types.ObjectId;
|
||||
secretIds?: Types.ObjectId[];
|
||||
}) => {
|
||||
@@ -162,7 +185,7 @@ const createActionHelper = async ({
|
||||
switch (name) {
|
||||
case ACTION_LOGIN:
|
||||
case ACTION_LOGOUT:
|
||||
action = await createActionUser({
|
||||
action = await createActionClient({
|
||||
name,
|
||||
userId
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Types } from 'mongoose';
|
||||
import _ from "lodash";
|
||||
import { Membership } from "../../models";
|
||||
import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization";
|
||||
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
|
||||
|
||||
export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => {
|
||||
export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => {
|
||||
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||
if (!membershipForWorkspace) {
|
||||
return false
|
||||
@@ -18,15 +19,15 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi
|
||||
return true
|
||||
}
|
||||
|
||||
export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, environment: any) => {
|
||||
export const userHasWriteOnlyAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
|
||||
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||
if (!membershipForWorkspace) {
|
||||
return false
|
||||
}
|
||||
|
||||
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
||||
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE });
|
||||
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ });
|
||||
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
|
||||
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
|
||||
|
||||
// case: you have write only if read is blocked and write is not
|
||||
if (isReadDisallowed && !isWriteDisallowed) {
|
||||
@@ -36,15 +37,15 @@ export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, env
|
||||
return false
|
||||
}
|
||||
|
||||
export const userHasNoAbility = async (userId: any, workspaceId: any, environment: any) => {
|
||||
export const userHasNoAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
|
||||
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||
if (!membershipForWorkspace) {
|
||||
return true
|
||||
}
|
||||
|
||||
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
||||
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE });
|
||||
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ });
|
||||
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
|
||||
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
|
||||
|
||||
if (isReadBlocked && isWriteDisallowed) {
|
||||
return true
|
||||
|
||||
@@ -16,12 +16,16 @@ import {
|
||||
*/
|
||||
const createLogHelper = async ({
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
}: {
|
||||
userId: Types.ObjectId;
|
||||
userId?: Types.ObjectId;
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
workspaceId?: Types.ObjectId;
|
||||
actions: IAction[];
|
||||
channel: string;
|
||||
@@ -31,6 +35,8 @@ const createLogHelper = async ({
|
||||
try {
|
||||
log = await new Log({
|
||||
user: userId,
|
||||
serviceAccount: serviceAccountId,
|
||||
serviceTokenData: serviceTokenDataId,
|
||||
workspace: workspaceId ?? undefined,
|
||||
actionNames: actions.map((a) => a.name),
|
||||
actions,
|
||||
|
||||
@@ -30,8 +30,8 @@ const requireSecretSnapshotAuth = ({
|
||||
}
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: secretSnapshot.workspace.toString(),
|
||||
userId: req.user._id,
|
||||
workspaceId: secretSnapshot.workspace,
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
export interface IAction {
|
||||
name: string;
|
||||
user?: Types.ObjectId,
|
||||
serviceAccount?: Types.ObjectId,
|
||||
serviceTokenData?: Types.ObjectId,
|
||||
workspace?: Types.ObjectId,
|
||||
payload?: {
|
||||
secretVersions?: Types.ObjectId[]
|
||||
@@ -33,8 +35,15 @@ const actionSchema = new Schema<IAction>(
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
ref: 'User'
|
||||
},
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceAccount'
|
||||
},
|
||||
serviceTokenData: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceTokenData'
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
export interface ILog {
|
||||
_id: Types.ObjectId;
|
||||
user?: Types.ObjectId;
|
||||
serviceAccount?: Types.ObjectId;
|
||||
serviceTokenData?: Types.ObjectId;
|
||||
workspace?: Types.ObjectId;
|
||||
actionNames: string[];
|
||||
actions: Types.ObjectId[];
|
||||
@@ -24,6 +26,14 @@ const logSchema = new Schema<ILog>(
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceAccount'
|
||||
},
|
||||
serviceTokenData: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceTokenData'
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace'
|
||||
|
||||
@@ -15,7 +15,8 @@ router.get(
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
query('offset').exists().isInt(),
|
||||
@@ -30,7 +31,8 @@ router.get(
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
@@ -43,7 +45,8 @@ router.post(
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('version').exists().isInt(),
|
||||
@@ -57,7 +60,8 @@ router.get(
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
query('offset').exists().isInt(),
|
||||
|
||||
@@ -26,12 +26,16 @@ class EELogService {
|
||||
*/
|
||||
static async createLog({
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
}: {
|
||||
userId: Types.ObjectId;
|
||||
userId?: Types.ObjectId;
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
workspaceId?: Types.ObjectId;
|
||||
actions: IAction[];
|
||||
channel: string;
|
||||
@@ -40,6 +44,8 @@ class EELogService {
|
||||
if (!EELicenseService.isLicenseValid) return null;
|
||||
return await createLogHelper({
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
actions,
|
||||
channel,
|
||||
@@ -59,17 +65,23 @@ class EELogService {
|
||||
static async createAction({
|
||||
name,
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
}: {
|
||||
name: string;
|
||||
userId: Types.ObjectId;
|
||||
userId?: Types.ObjectId;
|
||||
serviceAccountId?: Types.ObjectId;
|
||||
serviceTokenDataId?: Types.ObjectId;
|
||||
workspaceId?: Types.ObjectId;
|
||||
secretIds?: Types.ObjectId[];
|
||||
}) {
|
||||
return await createActionHelper({
|
||||
name,
|
||||
userId,
|
||||
serviceAccountId,
|
||||
serviceTokenDataId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
});
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcrypt';
|
||||
import {
|
||||
IUser,
|
||||
User,
|
||||
ServiceTokenData,
|
||||
ServiceAccount,
|
||||
APIKeyData
|
||||
} from '../models';
|
||||
import {
|
||||
AccountNotFoundError,
|
||||
ServiceTokenDataNotFoundError,
|
||||
ServiceAccountNotFoundError,
|
||||
APIKeyDataNotFoundError,
|
||||
UnauthorizedRequestError,
|
||||
BadRequestError
|
||||
@@ -20,6 +23,12 @@ import {
|
||||
getJwtRefreshLifetime,
|
||||
getJwtRefreshSecret
|
||||
} from '../config';
|
||||
import {
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../variables';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -37,7 +46,7 @@ const validateAuthMode = ({
|
||||
const apiKey = headers['x-api-key'];
|
||||
const authHeader = headers['authorization'];
|
||||
|
||||
let authTokenType, authTokenValue;
|
||||
let authMode, authTokenValue;
|
||||
if (apiKey === undefined && authHeader === undefined) {
|
||||
// case: no auth or X-API-KEY header present
|
||||
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
|
||||
@@ -45,7 +54,7 @@ const validateAuthMode = ({
|
||||
|
||||
if (typeof apiKey === 'string') {
|
||||
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
|
||||
authTokenType = 'apiKey';
|
||||
authMode = AUTH_MODE_API_KEY;
|
||||
authTokenValue = apiKey;
|
||||
}
|
||||
|
||||
@@ -61,20 +70,24 @@ const validateAuthMode = ({
|
||||
|
||||
switch (tokenValue.split('.', 1)[0]) {
|
||||
case 'st':
|
||||
authTokenType = 'serviceToken';
|
||||
authMode = AUTH_MODE_SERVICE_TOKEN;
|
||||
break;
|
||||
case 'sa':
|
||||
authMode = AUTH_MODE_SERVICE_ACCOUNT;
|
||||
break;
|
||||
default:
|
||||
authTokenType = 'jwt';
|
||||
authMode = AUTH_MODE_JWT;
|
||||
}
|
||||
|
||||
authTokenValue = tokenValue;
|
||||
}
|
||||
|
||||
if (!authTokenType || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
|
||||
if (!authMode || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
|
||||
|
||||
if (!acceptedAuthModes.includes(authTokenType)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
|
||||
if (!acceptedAuthModes.includes(authMode)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
|
||||
|
||||
return ({
|
||||
authTokenType,
|
||||
authMode,
|
||||
authTokenValue
|
||||
});
|
||||
}
|
||||
@@ -90,25 +103,17 @@ const getAuthUserPayload = async ({
|
||||
}: {
|
||||
authTokenValue: string;
|
||||
}) => {
|
||||
let user;
|
||||
try {
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(authTokenValue, getJwtAuthSecret())
|
||||
);
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(authTokenValue, getJwtAuthSecret())
|
||||
);
|
||||
|
||||
user = await User.findOne({
|
||||
_id: decodedToken.userId
|
||||
}).select('+publicKey');
|
||||
const user = await User.findOne({
|
||||
_id: decodedToken.userId
|
||||
}).select('+publicKey');
|
||||
|
||||
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
|
||||
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
|
||||
|
||||
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });
|
||||
|
||||
} catch (err) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate JWT token'
|
||||
});
|
||||
}
|
||||
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });
|
||||
|
||||
return user;
|
||||
}
|
||||
@@ -124,45 +129,70 @@ const getAuthSTDPayload = async ({
|
||||
}: {
|
||||
authTokenValue: string;
|
||||
}) => {
|
||||
let serviceTokenData;
|
||||
try {
|
||||
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||
|
||||
// TODO: optimize double query
|
||||
serviceTokenData = await ServiceTokenData
|
||||
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
|
||||
let serviceTokenData = await ServiceTokenData
|
||||
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
|
||||
|
||||
if (!serviceTokenData) {
|
||||
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
||||
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
|
||||
// case: service token expired
|
||||
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate expired service token'
|
||||
});
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
|
||||
if (!isMatch) throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate service token'
|
||||
});
|
||||
|
||||
serviceTokenData = await ServiceTokenData
|
||||
.findById(TOKEN_IDENTIFIER)
|
||||
.select('+encryptedKey +iv +tag')
|
||||
.populate<{user: IUser}>('user');
|
||||
|
||||
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
||||
|
||||
} catch (err) {
|
||||
if (!serviceTokenData) {
|
||||
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
||||
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
|
||||
// case: service token expired
|
||||
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate service token'
|
||||
message: 'Failed to authenticate expired service token'
|
||||
});
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
|
||||
if (!isMatch) throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate service token'
|
||||
});
|
||||
|
||||
serviceTokenData = await ServiceTokenData
|
||||
.findOneAndUpdate({
|
||||
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
|
||||
}, {
|
||||
lastUsed: new Date()
|
||||
}, {
|
||||
new: true
|
||||
})
|
||||
.select('+encryptedKey +iv +tag');
|
||||
|
||||
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
||||
|
||||
return serviceTokenData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return service account access key payload
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.authTokenValue - service account access token value
|
||||
* @returns {ServiceAccount} serviceAccount
|
||||
*/
|
||||
const getAuthSAAKPayload = async ({
|
||||
authTokenValue
|
||||
}: {
|
||||
authTokenValue: string;
|
||||
}) => {
|
||||
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||
|
||||
const serviceAccount = await ServiceAccount.findById(
|
||||
Buffer.from(TOKEN_IDENTIFIER, 'base64').toString('hex')
|
||||
).select('+secretHash');
|
||||
|
||||
if (!serviceAccount) {
|
||||
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
|
||||
}
|
||||
|
||||
const result = await bcrypt.compare(TOKEN_SECRET, serviceAccount.secretHash);
|
||||
if (!result) throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate service account access key'
|
||||
});
|
||||
|
||||
return serviceAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return API key data payload corresponding to API key [authTokenValue]
|
||||
* @param {Object} obj
|
||||
@@ -174,33 +204,44 @@ const getAuthAPIKeyPayload = async ({
|
||||
}: {
|
||||
authTokenValue: string;
|
||||
}) => {
|
||||
let user;
|
||||
try {
|
||||
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||
|
||||
const apiKeyData = await APIKeyData
|
||||
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
|
||||
.populate('user', '+publicKey');
|
||||
let apiKeyData = await APIKeyData
|
||||
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
|
||||
.populate<{user: IUser}>('user', '+publicKey');
|
||||
|
||||
if (!apiKeyData) {
|
||||
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
|
||||
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
|
||||
// case: API key expired
|
||||
await APIKeyData.findByIdAndDelete(apiKeyData._id);
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate expired API key'
|
||||
});
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
|
||||
if (!isMatch) throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate API key'
|
||||
});
|
||||
|
||||
user = apiKeyData.user;
|
||||
} catch (err) {
|
||||
if (!apiKeyData) {
|
||||
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
|
||||
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
|
||||
// case: API key expired
|
||||
await APIKeyData.findByIdAndDelete(apiKeyData._id);
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate API key'
|
||||
message: 'Failed to authenticate expired API key'
|
||||
});
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
|
||||
if (!isMatch) throw UnauthorizedRequestError({
|
||||
message: 'Failed to authenticate API key'
|
||||
});
|
||||
|
||||
apiKeyData = await APIKeyData.findOneAndUpdate({
|
||||
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
|
||||
}, {
|
||||
lastUsed: new Date()
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
|
||||
if (!apiKeyData) {
|
||||
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
|
||||
}
|
||||
|
||||
const user = await User.findById(apiKeyData.user).select('+publicKey');
|
||||
|
||||
if (!user) {
|
||||
throw AccountNotFoundError({
|
||||
message: 'Failed to find user'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -216,30 +257,23 @@ const getAuthAPIKeyPayload = async ({
|
||||
* @return {String} obj.refreshToken - issued refresh token
|
||||
*/
|
||||
const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
||||
let token: string;
|
||||
let refreshToken: string;
|
||||
try {
|
||||
// issue tokens
|
||||
token = createToken({
|
||||
payload: {
|
||||
userId
|
||||
},
|
||||
expiresIn: getJwtAuthLifetime(),
|
||||
secret: getJwtAuthSecret()
|
||||
});
|
||||
|
||||
refreshToken = createToken({
|
||||
payload: {
|
||||
userId
|
||||
},
|
||||
expiresIn: getJwtRefreshLifetime(),
|
||||
secret: getJwtRefreshSecret()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to issue tokens');
|
||||
}
|
||||
// issue tokens
|
||||
const token = createToken({
|
||||
payload: {
|
||||
userId
|
||||
},
|
||||
expiresIn: getJwtAuthLifetime(),
|
||||
secret: getJwtAuthSecret()
|
||||
});
|
||||
|
||||
const refreshToken = createToken({
|
||||
payload: {
|
||||
userId
|
||||
},
|
||||
expiresIn: getJwtRefreshLifetime(),
|
||||
secret: getJwtRefreshSecret()
|
||||
});
|
||||
|
||||
return {
|
||||
token,
|
||||
@@ -253,19 +287,14 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
||||
* @param {String} obj.userId - id of user whose tokens are cleared.
|
||||
*/
|
||||
const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
|
||||
try {
|
||||
// increment refreshVersion on user by 1
|
||||
User.findOneAndUpdate({
|
||||
_id: userId
|
||||
}, {
|
||||
$inc: {
|
||||
refreshVersion: 1
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
// increment refreshVersion on user by 1
|
||||
User.findOneAndUpdate({
|
||||
_id: userId
|
||||
}, {
|
||||
$inc: {
|
||||
refreshVersion: 1
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -285,21 +314,16 @@ const createToken = ({
|
||||
expiresIn: string | number;
|
||||
secret: string;
|
||||
}) => {
|
||||
try {
|
||||
return jwt.sign(payload, secret, {
|
||||
expiresIn
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create a token');
|
||||
}
|
||||
return jwt.sign(payload, secret, {
|
||||
expiresIn
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
validateAuthMode,
|
||||
getAuthUserPayload,
|
||||
getAuthSTDPayload,
|
||||
getAuthSAAKPayload,
|
||||
getAuthAPIKeyPayload,
|
||||
createToken,
|
||||
issueAuthTokens,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { Membership, Key } from '../models';
|
||||
import {
|
||||
MembershipNotFoundError,
|
||||
BadRequestError
|
||||
} from '../utils/errors';
|
||||
|
||||
/**
|
||||
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
||||
@@ -14,28 +19,24 @@ const validateMembership = async ({
|
||||
workspaceId,
|
||||
acceptedRoles,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
acceptedRoles: string[];
|
||||
userId: Types.ObjectId;
|
||||
workspaceId: Types.ObjectId;
|
||||
acceptedRoles?: string[];
|
||||
}) => {
|
||||
|
||||
let membership;
|
||||
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
|
||||
try {
|
||||
membership = await Membership.findOne({
|
||||
user: userId,
|
||||
workspace: workspaceId
|
||||
}).populate("workspace");
|
||||
|
||||
if (!membership) throw new Error('Failed to find membership');
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: userId,
|
||||
workspace: workspaceId
|
||||
}).populate("workspace");
|
||||
|
||||
if (!membership) {
|
||||
throw MembershipNotFoundError({ message: 'Failed to find workspace membership' });
|
||||
}
|
||||
|
||||
if (acceptedRoles) {
|
||||
if (!acceptedRoles.includes(membership.role)) {
|
||||
throw new Error('Failed to validate membership role');
|
||||
throw BadRequestError({ message: 'Failed to validate workspace membership role' });
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to validate membership');
|
||||
}
|
||||
|
||||
return membership;
|
||||
|
||||
@@ -1,40 +1,48 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { MembershipOrg, Workspace, Membership, Key } from '../models';
|
||||
import {
|
||||
MembershipOrgNotFoundError,
|
||||
BadRequestError
|
||||
} from '../utils/errors';
|
||||
|
||||
/**
|
||||
* Validate that user with id [userId] is a member of organization with id [organizationId]
|
||||
* and has at least one of the roles in [acceptedRoles]
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.userId
|
||||
* @param {Types.ObjectId} obj.organizationId
|
||||
* @param {String[]} obj.acceptedRoles
|
||||
*/
|
||||
const validateMembership = async ({
|
||||
const validateMembershipOrg = async ({
|
||||
userId,
|
||||
organizationId,
|
||||
acceptedRoles
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
}: {
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
userId: Types.ObjectId;
|
||||
organizationId: Types.ObjectId;
|
||||
acceptedRoles: string[];
|
||||
acceptedStatuses: 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');
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: userId,
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
if (!membershipOrg) {
|
||||
throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' });
|
||||
}
|
||||
|
||||
return membership;
|
||||
if (!acceptedRoles.includes(membershipOrg.role)) {
|
||||
throw BadRequestError({ message: 'Failed to validate organization membership role' });
|
||||
}
|
||||
|
||||
if (!acceptedStatuses.includes(membershipOrg.status)) {
|
||||
throw BadRequestError({ message: 'Failed to validate organization membership status' });
|
||||
}
|
||||
|
||||
return membershipOrg;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +164,7 @@ const deleteMembershipOrg = async ({
|
||||
};
|
||||
|
||||
export {
|
||||
validateMembership,
|
||||
validateMembershipOrg,
|
||||
findMembershipOrg,
|
||||
addMembershipsOrg,
|
||||
deleteMembershipOrg
|
||||
|
||||
@@ -1,14 +1,70 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import Stripe from 'stripe';
|
||||
import { Types } from 'mongoose';
|
||||
import { ACCEPTED } from '../variables';
|
||||
import {
|
||||
IUser,
|
||||
User,
|
||||
IServiceAccount,
|
||||
ServiceAccount,
|
||||
IServiceTokenData,
|
||||
ServiceTokenData
|
||||
} from '../models';
|
||||
import { Organization, MembershipOrg } from '../models';
|
||||
import {
|
||||
ACCEPTED,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../variables';
|
||||
import {
|
||||
getStripeSecretKey,
|
||||
getStripeProductPro,
|
||||
getStripeProductTeam,
|
||||
getStripeProductStarter
|
||||
} from '../config';
|
||||
import {
|
||||
UnauthorizedRequestError
|
||||
} from '../utils/errors';
|
||||
|
||||
/**
|
||||
* Validate accepted clients for organization with id [organizationId]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.authData - authenticated client details
|
||||
* @param {Types.ObjectId} obj.organizationId - id of organization to validate against
|
||||
*/
|
||||
const validateClientForOrganization = async ({
|
||||
authData,
|
||||
organizationId
|
||||
}: {
|
||||
authData: {
|
||||
authMode: string;
|
||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||
},
|
||||
organizationId: string;
|
||||
}) => {
|
||||
// TODO
|
||||
|
||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed client authorization for organization resource'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an organization with name [name]
|
||||
|
||||
@@ -21,60 +21,8 @@ import {
|
||||
ACTION_READ_SECRETS
|
||||
} from '../variables';
|
||||
import _ from 'lodash';
|
||||
import { ABILITY_WRITE } from '../variables/organization';
|
||||
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||
|
||||
/**
|
||||
* Validate that user with id [userId] can modify secrets with ids [secretIds]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.userId - id of user to validate
|
||||
* @param {Object} obj.secretIds - secret ids
|
||||
* @returns {Secret[]} secrets
|
||||
*/
|
||||
const validateSecrets = async ({
|
||||
userId,
|
||||
secretIds
|
||||
}: {
|
||||
userId: string;
|
||||
secretIds: string[];
|
||||
}) => {
|
||||
let secrets;
|
||||
try {
|
||||
secrets = await Secret.find({
|
||||
_id: {
|
||||
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
|
||||
}
|
||||
});
|
||||
|
||||
if (secrets.length != secretIds.length) {
|
||||
throw BadRequestError({ message: 'Unable to validate some secrets' })
|
||||
}
|
||||
|
||||
const userMemberships = await Membership.find({ user: userId })
|
||||
const userMembershipById = _.keyBy(userMemberships, 'workspace');
|
||||
const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString()));
|
||||
|
||||
// for each secret check if the secret belongs to a workspace the user is a member of
|
||||
secrets.forEach((secret: ISecret) => {
|
||||
if (workspaceIdsSet.has(secret.workspace.toString())) {
|
||||
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
|
||||
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: ABILITY_WRITE });
|
||||
|
||||
if (isDisallowed) {
|
||||
throw UnauthorizedRequestError({ message: 'You do not have the required permissions to perform this action' });
|
||||
}
|
||||
} else {
|
||||
throw BadRequestError({ message: 'You cannot edit secrets of a workspace you are not a member of' });
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
throw BadRequestError({ message: 'Unable to validate secrets' })
|
||||
}
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
interface V1PushSecret {
|
||||
ciphertextKey: string;
|
||||
ivKey: string;
|
||||
@@ -714,7 +662,6 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
||||
};
|
||||
|
||||
export {
|
||||
validateSecrets,
|
||||
v1PushSecrets,
|
||||
v2PushSecrets,
|
||||
pullSecrets,
|
||||
|
||||
109
backend/src/helpers/secrets.ts
Normal file
109
backend/src/helpers/secrets.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
User,
|
||||
IUser,
|
||||
ServiceAccount,
|
||||
IServiceAccount,
|
||||
ServiceTokenData,
|
||||
IServiceTokenData,
|
||||
Secret,
|
||||
ISecret
|
||||
} from '../models';
|
||||
import {
|
||||
validateUserClientForSecrets
|
||||
} from '../helpers/user';
|
||||
import {
|
||||
validateServiceTokenDataClientForSecrets
|
||||
} from '../helpers/serviceTokenData';
|
||||
import {
|
||||
validateServiceAccountClientForSecrets
|
||||
} from '../helpers/serviceAccount';
|
||||
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||
import {
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../variables';
|
||||
|
||||
/**
|
||||
* Validate accepted clients for secrets with ids [secretIds]
|
||||
* @param {Object} obj
|
||||
* @param {User} obj.user - user client
|
||||
* @param {ServiceAccount} obj.serviceAccount - service account client
|
||||
* @param {ServiceTokenData} obj.service - service token client
|
||||
* @param {String[]} obj.secretIds - ids of secrets to validate against
|
||||
*/
|
||||
const validateClientForSecrets = async ({
|
||||
authData,
|
||||
secretIds,
|
||||
requiredPermissions
|
||||
}: {
|
||||
authData: {
|
||||
authMode: string;
|
||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||
},
|
||||
secretIds: string[];
|
||||
requiredPermissions: string[];
|
||||
}) => {
|
||||
|
||||
let secrets: ISecret[] = [];
|
||||
|
||||
secrets = await Secret.find({
|
||||
_id: {
|
||||
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
|
||||
}
|
||||
});
|
||||
|
||||
if (secrets.length != secretIds.length) {
|
||||
throw BadRequestError({ message: 'Failed to validate non-existent secrets' })
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||
await validateUserClientForSecrets({
|
||||
user: authData.authPayload,
|
||||
secrets,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||
await validateServiceAccountClientForSecrets({
|
||||
serviceAccount: authData.authPayload,
|
||||
secrets,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||
await validateServiceTokenDataClientForSecrets({
|
||||
serviceTokenData: authData.authPayload,
|
||||
secrets,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||
await validateUserClientForSecrets({
|
||||
user: authData.authPayload,
|
||||
secrets,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed client authorization for secrets resource'
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
validateClientForSecrets
|
||||
}
|
||||
239
backend/src/helpers/serviceAccount.ts
Normal file
239
backend/src/helpers/serviceAccount.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import _ from 'lodash';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
User,
|
||||
IUser,
|
||||
ServiceAccount,
|
||||
IServiceAccount,
|
||||
ServiceTokenData,
|
||||
IServiceTokenData,
|
||||
ISecret,
|
||||
ServiceAccountWorkspacePermission
|
||||
} from '../models';
|
||||
import {
|
||||
BadRequestError,
|
||||
UnauthorizedRequestError,
|
||||
ServiceAccountNotFoundError
|
||||
} from '../utils/errors';
|
||||
import {
|
||||
PERMISSION_READ_SECRETS,
|
||||
PERMISSION_WRITE_SECRETS,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../variables';
|
||||
import {
|
||||
validateUserClientForServiceAccount
|
||||
} from '../helpers/user';
|
||||
|
||||
const validateClientForServiceAccount = async ({
|
||||
authData,
|
||||
serviceAccountId,
|
||||
requiredPermissions
|
||||
}: {
|
||||
authData: {
|
||||
authMode: string;
|
||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||
},
|
||||
serviceAccountId: Types.ObjectId;
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
|
||||
|
||||
if (!serviceAccount) {
|
||||
throw ServiceAccountNotFoundError({
|
||||
message: 'Failed to find service account'
|
||||
});
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||
await validateUserClientForServiceAccount({
|
||||
user: authData.authPayload,
|
||||
serviceAccount,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return serviceAccount;
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||
await validateServiceAccountClientForServiceAccount({
|
||||
serviceAccount: authData.authPayload,
|
||||
targetServiceAccount: serviceAccount,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return serviceAccount;
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed service token authorization for service account resource'
|
||||
});
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||
await validateUserClientForServiceAccount({
|
||||
user: authData.authPayload,
|
||||
serviceAccount,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return serviceAccount;
|
||||
}
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed client authorization for service account resource'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that service account (client) can access workspace
|
||||
* with id [workspaceId] and its environment [environment] with required permissions
|
||||
* [requiredPermissions]
|
||||
* @param {Object} obj
|
||||
* @param {ServiceAccount} obj.serviceAccount - service account client
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
|
||||
* @param {String} environment - (optional) environment in workspace to validate against
|
||||
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
||||
*/
|
||||
const validateServiceAccountClientForWorkspace = async ({
|
||||
serviceAccount,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions
|
||||
}: {
|
||||
serviceAccount: IServiceAccount;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
// TODO: add service account API support for workspace-level endpoints that are not
|
||||
// tied to any specific environment
|
||||
|
||||
if (environment) {
|
||||
const permission = await ServiceAccountWorkspacePermission.findOne({
|
||||
serviceAccount,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment
|
||||
});
|
||||
|
||||
if (!permission) throw UnauthorizedRequestError({
|
||||
message: 'Failed service account authorization for the given workspace environment'
|
||||
});
|
||||
|
||||
// TODO: refactor
|
||||
let runningIsDisallowed = false;
|
||||
requiredPermissions?.forEach((requiredPermission: string) => {
|
||||
switch (requiredPermission) {
|
||||
case PERMISSION_READ_SECRETS:
|
||||
if (!permission.read) runningIsDisallowed = true;
|
||||
break;
|
||||
case PERMISSION_WRITE_SECRETS:
|
||||
if (!permission.write) runningIsDisallowed = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (runningIsDisallowed) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that service account (client) can access secrets
|
||||
* with required permissions [requiredPermissions]
|
||||
* @param {Object} obj
|
||||
* @param {ServiceAccount} obj.serviceAccount - service account client
|
||||
* @param {Secret[]} secrets - secrets to validate against
|
||||
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
|
||||
*/
|
||||
const validateServiceAccountClientForSecrets = async ({
|
||||
serviceAccount,
|
||||
secrets,
|
||||
requiredPermissions
|
||||
}: {
|
||||
serviceAccount: IServiceAccount;
|
||||
secrets: ISecret[];
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
|
||||
const permissions = await ServiceAccountWorkspacePermission.find({
|
||||
serviceAccount: serviceAccount._id
|
||||
});
|
||||
|
||||
const permissionsObj = _.keyBy(permissions, (p) => {
|
||||
return `${p.workspace.toString()}-${p.environment}`
|
||||
});
|
||||
|
||||
secrets.forEach((secret: ISecret) => {
|
||||
const permission = permissionsObj[`${secret.workspace.toString()}-${secret.environment}`];
|
||||
|
||||
if (!permission) throw BadRequestError({
|
||||
message: 'Failed to find any permission for the secret workspace and environment'
|
||||
});
|
||||
|
||||
requiredPermissions?.forEach((requiredPermission: string) => {
|
||||
// TODO: refactor
|
||||
let runningIsDisallowed = false;
|
||||
requiredPermissions?.forEach((requiredPermission: string) => {
|
||||
switch (requiredPermission) {
|
||||
case PERMISSION_READ_SECRETS:
|
||||
if (!permission.read) runningIsDisallowed = true;
|
||||
break;
|
||||
case PERMISSION_WRITE_SECRETS:
|
||||
if (!permission.write) runningIsDisallowed = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (runningIsDisallowed) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that service account (client) can access target service
|
||||
* account [serviceAccount] with required permissions [requiredPermissions]
|
||||
* @param {Object} obj
|
||||
* @param {SerivceAccount} obj.serviceAccount - service account client
|
||||
* @param {ServiceAccount} targetServiceAccount - target service account to validate against
|
||||
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
|
||||
*/
|
||||
const validateServiceAccountClientForServiceAccount = ({
|
||||
serviceAccount,
|
||||
targetServiceAccount,
|
||||
requiredPermissions
|
||||
}: {
|
||||
serviceAccount: IServiceAccount;
|
||||
targetServiceAccount: IServiceAccount;
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
if (!serviceAccount.organization.equals(targetServiceAccount.organization)) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed service account authorization for the given service account'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
validateClientForServiceAccount,
|
||||
validateServiceAccountClientForWorkspace,
|
||||
validateServiceAccountClientForSecrets,
|
||||
validateServiceAccountClientForServiceAccount
|
||||
}
|
||||
99
backend/src/helpers/serviceTokenData.ts
Normal file
99
backend/src/helpers/serviceTokenData.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
ISecret,
|
||||
IServiceTokenData
|
||||
} from '../models';
|
||||
import { UnauthorizedRequestError } from '../utils/errors';
|
||||
|
||||
/**
|
||||
* Validate that service token (client) can access workspace
|
||||
* with id [workspaceId] and its environment [environment] with required permissions
|
||||
* [requiredPermissions]
|
||||
* @param {Object} obj
|
||||
* @param {ServiceTokenData} obj.serviceTokenData - service token client
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
|
||||
* @param {String} environment - (optional) environment in workspace to validate against
|
||||
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
||||
*/
|
||||
const validateServiceTokenDataClientForWorkspace = async ({
|
||||
serviceTokenData,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions
|
||||
}: {
|
||||
serviceTokenData: IServiceTokenData;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
|
||||
if (!serviceTokenData.workspace.equals(workspaceId)) {
|
||||
// case: invalid workspaceId passed
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed service token authorization for the given workspace'
|
||||
});
|
||||
}
|
||||
|
||||
if (serviceTokenData.environment !== environment) {
|
||||
// case: invalid environment passed
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed service token authorization for the given workspace environment'
|
||||
});
|
||||
}
|
||||
|
||||
requiredPermissions?.forEach((permission) => {
|
||||
if (!serviceTokenData.permissions.includes(permission)) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: `Failed service token authorization for the given workspace environment action: ${permission}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that service token (client) can access secrets
|
||||
* with required permissions [requiredPermissions]
|
||||
* @param {Object} obj
|
||||
* @param {ServiceTokenData} obj.serviceTokenData - service token client
|
||||
* @param {Secret[]} secrets - secrets to validate against
|
||||
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
|
||||
*/
|
||||
const validateServiceTokenDataClientForSecrets = async ({
|
||||
serviceTokenData,
|
||||
secrets,
|
||||
requiredPermissions
|
||||
}: {
|
||||
serviceTokenData: IServiceTokenData;
|
||||
secrets: ISecret[];
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
|
||||
secrets.forEach((secret: ISecret) => {
|
||||
if (!serviceTokenData.workspace.equals(secret.workspace)) {
|
||||
// case: invalid workspaceId passed
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed service token authorization for the given workspace'
|
||||
});
|
||||
}
|
||||
|
||||
if (serviceTokenData.environment !== secret.environment) {
|
||||
// case: invalid environment passed
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed service token authorization for the given workspace environment'
|
||||
});
|
||||
}
|
||||
|
||||
requiredPermissions?.forEach((permission) => {
|
||||
if (!serviceTokenData.permissions.includes(permission)) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: `Failed service token authorization for the given workspace environment action: ${permission}`
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
validateServiceTokenDataClientForWorkspace,
|
||||
validateServiceTokenDataClientForSecrets
|
||||
}
|
||||
0
backend/src/helpers/telemetry.ts
Normal file
0
backend/src/helpers/telemetry.ts
Normal file
@@ -1,6 +1,23 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { IUser, User } from '../models';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
IUser,
|
||||
ISecret,
|
||||
IServiceAccount,
|
||||
User,
|
||||
Membership
|
||||
} from '../models';
|
||||
import { sendMail } from './nodemailer';
|
||||
import { validateMembership } from './membership';
|
||||
import _ from 'lodash';
|
||||
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||
import {
|
||||
validateMembershipOrg
|
||||
} from '../helpers/membershipOrg';
|
||||
import {
|
||||
PERMISSION_READ_SECRETS,
|
||||
PERMISSION_WRITE_SECRETS
|
||||
} from '../variables';
|
||||
|
||||
/**
|
||||
* Initialize a user under email [email]
|
||||
@@ -146,4 +163,136 @@ const checkUserDevice = async ({
|
||||
}
|
||||
}
|
||||
|
||||
export { setupAccount, completeAccount, checkUserDevice };
|
||||
/**
|
||||
* Validate that user (client) can access workspace
|
||||
* with id [workspaceId] and its environment [environment] with required permissions
|
||||
* [requiredPermissions]
|
||||
* @param {Object} obj
|
||||
* @param {User} obj.user - user client
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
|
||||
* @param {String} environment - (optional) environment in workspace to validate against
|
||||
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
||||
*/
|
||||
const validateUserClientForWorkspace = async ({
|
||||
user,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions
|
||||
}: {
|
||||
user: IUser;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
|
||||
// validate user membership in workspace
|
||||
const membership = await validateMembership({
|
||||
userId: user._id,
|
||||
workspaceId
|
||||
});
|
||||
|
||||
// TODO: refactor
|
||||
let runningIsDisallowed = false;
|
||||
requiredPermissions?.forEach((requiredPermission: string) => {
|
||||
switch (requiredPermission) {
|
||||
case PERMISSION_READ_SECRETS:
|
||||
runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
|
||||
break;
|
||||
case PERMISSION_WRITE_SECRETS:
|
||||
runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (runningIsDisallowed) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return membership;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that user (client) can access secrets [secrets]
|
||||
* with required permissions [requiredPermissions]
|
||||
* @param {Object} obj
|
||||
* @param {User} obj.user - user client
|
||||
* @param {Secret[]} obj.secrets - secrets to validate against
|
||||
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
||||
*/
|
||||
const validateUserClientForSecrets = async ({
|
||||
user,
|
||||
secrets,
|
||||
requiredPermissions
|
||||
}: {
|
||||
user: IUser;
|
||||
secrets: ISecret[];
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
// TODO: refactor
|
||||
|
||||
const userMemberships = await Membership.find({ user: user._id })
|
||||
const userMembershipById = _.keyBy(userMemberships, 'workspace');
|
||||
const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString()));
|
||||
|
||||
// for each secret check if the secret belongs to a workspace the user is a member of
|
||||
secrets.forEach((secret: ISecret) => {
|
||||
if (!workspaceIdsSet.has(secret.workspace.toString())) {
|
||||
throw BadRequestError({
|
||||
message: 'Failed authorization for the secret'
|
||||
});
|
||||
}
|
||||
|
||||
if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) {
|
||||
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
|
||||
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS });
|
||||
|
||||
if (isDisallowed) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'You do not have the required permissions to perform this action'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that user (client) can access service account [serviceAccount]
|
||||
* with required permissions [requiredPermissions]
|
||||
* @param {Object} obj
|
||||
* @param {User} obj.user - user client
|
||||
* @param {ServiceAccount} obj.serviceAccount - service account to validate against
|
||||
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
||||
*/
|
||||
const validateUserClientForServiceAccount = async ({
|
||||
user,
|
||||
serviceAccount,
|
||||
requiredPermissions
|
||||
}: {
|
||||
user: IUser;
|
||||
serviceAccount: IServiceAccount;
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
if (!serviceAccount.user.equals(user._id)) {
|
||||
// case: user who created service account is not the
|
||||
// same user that is on the request
|
||||
await validateMembershipOrg({
|
||||
userId: user._id,
|
||||
organizationId: serviceAccount.organization,
|
||||
acceptedRoles: [],
|
||||
acceptedStatuses: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
setupAccount,
|
||||
completeAccount,
|
||||
checkUserDevice,
|
||||
validateUserClientForWorkspace,
|
||||
validateUserClientForSecrets,
|
||||
validateUserClientForServiceAccount
|
||||
};
|
||||
|
||||
@@ -1,12 +1,104 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Workspace,
|
||||
Bot,
|
||||
Membership,
|
||||
Key,
|
||||
Secret
|
||||
Secret,
|
||||
User,
|
||||
IUser,
|
||||
ServiceAccountWorkspacePermission,
|
||||
ServiceAccount,
|
||||
IServiceAccount,
|
||||
ServiceTokenData,
|
||||
IServiceTokenData,
|
||||
} from '../models';
|
||||
import { createBot } from '../helpers/bot';
|
||||
import { validateUserClientForWorkspace } from '../helpers/user';
|
||||
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
|
||||
import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData';
|
||||
import { validateMembership } from '../helpers/membership';
|
||||
import { UnauthorizedRequestError } from '../utils/errors';
|
||||
import {
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../variables';
|
||||
|
||||
/**
|
||||
* Validate authenticated clients for workspace with id [workspaceId] based
|
||||
* on any known permissions.
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.authData - authenticated client details
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
|
||||
* @param {String} obj.environment - (optional) environment in workspace to validate against
|
||||
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
|
||||
*/
|
||||
const validateClientForWorkspace = async ({
|
||||
authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions
|
||||
}: {
|
||||
authData: {
|
||||
authMode: string;
|
||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||
},
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
|
||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||
const membership = await validateUserClientForWorkspace({
|
||||
user: authData.authPayload,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return ({ membership });
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||
await validateServiceAccountClientForWorkspace({
|
||||
serviceAccount: authData.authPayload,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: authData.authPayload,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||
const membership = await validateUserClientForWorkspace({
|
||||
user: authData.authPayload,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return ({ membership });
|
||||
}
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed client authorization for workspace resource'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a workspace with name [name] in organization with id [organizationId]
|
||||
@@ -71,4 +163,8 @@ const deleteWorkspace = async ({ id }: { id: string }) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { createWorkspace, deleteWorkspace };
|
||||
export {
|
||||
validateClientForWorkspace,
|
||||
createWorkspace,
|
||||
deleteWorkspace
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as Sentry from '@sentry/node';
|
||||
import { DatabaseService } from './services';
|
||||
import { setUpHealthEndpoint } from './services/health';
|
||||
import { initSmtp } from './services/smtp';
|
||||
import { logTelemetryMessage } from './services';
|
||||
import { TelemetryService } from './services';
|
||||
import { setTransporter } from './helpers/nodemailer';
|
||||
import { createTestUserForDevelopment } from './utils/addDevelopmentUser';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
@@ -48,17 +48,18 @@ import {
|
||||
integrationAuth as v1IntegrationAuthRouter
|
||||
} from './routes/v1';
|
||||
import {
|
||||
signup as v2SignupRouter,
|
||||
auth as v2AuthRouter,
|
||||
users as v2UsersRouter,
|
||||
organizations as v2OrganizationsRouter,
|
||||
workspace as v2WorkspaceRouter,
|
||||
secret as v2SecretRouter, // begin to phase out
|
||||
secrets as v2SecretsRouter,
|
||||
serviceTokenData as v2ServiceTokenDataRouter,
|
||||
apiKeyData as v2APIKeyDataRouter,
|
||||
environment as v2EnvironmentRouter,
|
||||
tags as v2TagsRouter,
|
||||
signup as v2SignupRouter,
|
||||
auth as v2AuthRouter,
|
||||
users as v2UsersRouter,
|
||||
organizations as v2OrganizationsRouter,
|
||||
workspace as v2WorkspaceRouter,
|
||||
secret as v2SecretRouter, // begin to phase out
|
||||
secrets as v2SecretsRouter,
|
||||
serviceTokenData as v2ServiceTokenDataRouter,
|
||||
serviceAccounts as v2ServiceAccountsRouter,
|
||||
apiKeyData as v2APIKeyDataRouter,
|
||||
environment as v2EnvironmentRouter,
|
||||
tags as v2TagsRouter,
|
||||
} from './routes/v2';
|
||||
import { healthCheck } from './routes/status';
|
||||
import { getLogger } from './utils/logger';
|
||||
@@ -79,7 +80,7 @@ const main = async () => {
|
||||
});
|
||||
}
|
||||
|
||||
logTelemetryMessage();
|
||||
TelemetryService.logTelemetryMessage();
|
||||
setTransporter(initSmtp());
|
||||
|
||||
await DatabaseService.initDatabase(getMongoURL());
|
||||
@@ -150,6 +151,7 @@ const main = async () => {
|
||||
app.use('/api/v2/secret', v2SecretRouter); // deprecated
|
||||
app.use('/api/v2/secrets', v2SecretsRouter);
|
||||
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
|
||||
app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new
|
||||
app.use('/api/v2/api-key', v2APIKeyDataRouter);
|
||||
|
||||
// api docs
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_RAILWAY,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_TRAVISCI,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
INTEGRATION_RAILWAY_API_URL,
|
||||
INTEGRATION_FLYIO_API_URL,
|
||||
INTEGRATION_CIRCLECI_API_URL,
|
||||
INTEGRATION_TRAVISCI_API_URL,
|
||||
@@ -94,6 +96,11 @@ const getApps = async ({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_RAILWAY:
|
||||
apps = await getAppsRailway({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_FLYIO:
|
||||
apps = await getAppsFlyio({
|
||||
accessToken,
|
||||
@@ -323,6 +330,58 @@ const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
|
||||
return apps;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of projects for Railway integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - access token for Railway API
|
||||
* @returns {Object[]} apps - names and ids of Railway services
|
||||
* @returns {String} apps.name - name of Railway project
|
||||
* @returns {String} apps.appId - id of Railway project
|
||||
*
|
||||
*/
|
||||
const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
|
||||
let apps: any[] = [];
|
||||
try {
|
||||
const query = `
|
||||
query GetProjects($userId: String, $teamId: String) {
|
||||
projects(userId: $userId, teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const variables = {};
|
||||
|
||||
const { data: { data: { projects: { edges }}} } = await request.post(INTEGRATION_RAILWAY_API_URL, {
|
||||
query,
|
||||
variables,
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
});
|
||||
|
||||
apps = edges.map((e: any) => ({
|
||||
name: e.node.name,
|
||||
appId: e.node.id
|
||||
}));
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to get Railway services");
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of apps for Fly.io integration
|
||||
* @param {Object} obj
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_RAILWAY,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_TRAVISCI,
|
||||
@@ -29,11 +30,13 @@ import {
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
INTEGRATION_RAILWAY_API_URL,
|
||||
INTEGRATION_FLYIO_API_URL,
|
||||
INTEGRATION_CIRCLECI_API_URL,
|
||||
INTEGRATION_TRAVISCI_API_URL,
|
||||
} from "../variables";
|
||||
import request from '../config/request';
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to [app] in integration named [integration]
|
||||
@@ -126,6 +129,13 @@ const syncSecrets = async ({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_RAILWAY:
|
||||
await syncSecretsRailway({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_FLYIO:
|
||||
await syncSecretsFlyio({
|
||||
integration,
|
||||
@@ -1152,6 +1162,58 @@ const syncSecretsRender = async ({
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Railway project with id [integration.appId]
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
* @param {String} obj.accessToken - access token for Railway integration
|
||||
*/
|
||||
const syncSecretsRailway = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
secrets: any;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
|
||||
const query = `
|
||||
mutation UpsertVariables($input: VariableCollectionUpsertInput!) {
|
||||
variableCollectionUpsert(input: $input)
|
||||
}
|
||||
`;
|
||||
|
||||
const input = {
|
||||
projectId: integration.appId,
|
||||
environmentId: integration.targetEnvironmentId,
|
||||
...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}),
|
||||
replace: true,
|
||||
variables: secrets
|
||||
};
|
||||
|
||||
await request.post(INTEGRATION_RAILWAY_API_URL, {
|
||||
query,
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error("Failed to sync secrets to Railway");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Fly.io app
|
||||
* @param {Object} obj
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
interface AddServiceAccountPermissionDto {
|
||||
name: string;
|
||||
workspaceId?: string;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
export default AddServiceAccountPermissionDto;
|
||||
@@ -0,0 +1,8 @@
|
||||
interface CreateServiceAccountDto {
|
||||
organizationId: string;
|
||||
name: string;
|
||||
publicKey: string;
|
||||
expiresIn: number;
|
||||
}
|
||||
|
||||
export default CreateServiceAccountDto;
|
||||
7
backend/src/interfaces/serviceAccounts/dto/index.ts
Normal file
7
backend/src/interfaces/serviceAccounts/dto/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import CreateServiceAccountDto from './CreateServiceAccountDto';
|
||||
import AddServiceAccountPermissionDto from './AddServiceAccountPermissionDto';
|
||||
|
||||
export {
|
||||
CreateServiceAccountDto,
|
||||
AddServiceAccountPermissionDto
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import requireIntegrationAuth from './requireIntegrationAuth';
|
||||
import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth';
|
||||
import requireServiceTokenAuth from './requireServiceTokenAuth';
|
||||
import requireServiceTokenDataAuth from './requireServiceTokenDataAuth';
|
||||
import requireServiceAccountAuth from './requireServiceAccountAuth';
|
||||
import requireServiceAccountWorkspacePermissionAuth from './requireServiceAccountWorkspacePermissionAuth';
|
||||
import requireSecretAuth from './requireSecretAuth';
|
||||
import requireSecretsAuth from './requireSecretsAuth';
|
||||
import validateRequest from './validateRequest';
|
||||
@@ -27,6 +29,8 @@ export {
|
||||
requireIntegrationAuthorizationAuth,
|
||||
requireServiceTokenAuth,
|
||||
requireServiceTokenDataAuth,
|
||||
requireServiceAccountAuth,
|
||||
requireServiceAccountWorkspacePermissionAuth,
|
||||
requireSecretAuth,
|
||||
requireSecretsAuth,
|
||||
validateRequest
|
||||
|
||||
@@ -4,11 +4,23 @@ import {
|
||||
validateAuthMode,
|
||||
getAuthUserPayload,
|
||||
getAuthSTDPayload,
|
||||
getAuthAPIKeyPayload
|
||||
getAuthAPIKeyPayload,
|
||||
getAuthSAAKPayload
|
||||
} from '../helpers/auth';
|
||||
import {
|
||||
UnauthorizedRequestError
|
||||
} from '../utils/errors';
|
||||
import {
|
||||
IUser,
|
||||
IServiceAccount,
|
||||
IServiceTokenData
|
||||
} from '../models';
|
||||
import {
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../variables';
|
||||
|
||||
declare module 'jsonwebtoken' {
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
@@ -27,50 +39,57 @@ declare module 'jsonwebtoken' {
|
||||
* @returns
|
||||
*/
|
||||
const requireAuth = ({
|
||||
acceptedAuthModes = ['jwt'],
|
||||
requiredServiceTokenPermissions = []
|
||||
acceptedAuthModes = [AUTH_MODE_JWT],
|
||||
}: {
|
||||
acceptedAuthModes: string[];
|
||||
requiredServiceTokenPermissions?: string[];
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
// validate auth token against accepted auth modes [acceptedAuthModes]
|
||||
// and return token type [authTokenType] and value [authTokenValue]
|
||||
const { authTokenType, authTokenValue } = validateAuthMode({
|
||||
const { authMode, authTokenValue } = validateAuthMode({
|
||||
headers: req.headers,
|
||||
acceptedAuthModes
|
||||
});
|
||||
|
||||
// attach auth payloads
|
||||
let serviceTokenData: any;
|
||||
switch (authTokenType) {
|
||||
case 'serviceToken':
|
||||
serviceTokenData = await getAuthSTDPayload({
|
||||
let authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||
switch (authMode) {
|
||||
case AUTH_MODE_SERVICE_ACCOUNT:
|
||||
authPayload = await getAuthSAAKPayload({
|
||||
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;
|
||||
|
||||
req.serviceAccount = authPayload;
|
||||
break;
|
||||
case 'apiKey':
|
||||
req.user = await getAuthAPIKeyPayload({
|
||||
case AUTH_MODE_SERVICE_TOKEN:
|
||||
authPayload = await getAuthSTDPayload({
|
||||
authTokenValue
|
||||
});
|
||||
req.serviceTokenData = authPayload;
|
||||
break;
|
||||
case AUTH_MODE_API_KEY:
|
||||
authPayload = await getAuthAPIKeyPayload({
|
||||
authTokenValue
|
||||
});
|
||||
req.user = authPayload;
|
||||
break;
|
||||
default:
|
||||
req.user = await getAuthUserPayload({
|
||||
authPayload = await getAuthUserPayload({
|
||||
authTokenValue
|
||||
});
|
||||
req.user = authPayload;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
req.requestData = {
|
||||
...req.params,
|
||||
...req.query,
|
||||
...req.body,
|
||||
}
|
||||
|
||||
req.authData = {
|
||||
authMode,
|
||||
authPayload
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ const requireBotAuth = ({
|
||||
}
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: bot.workspace.toString(),
|
||||
userId: req.user._id,
|
||||
workspaceId: bot.workspace,
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ const requireIntegrationAuth = ({
|
||||
}
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: integration.workspace.toString(),
|
||||
userId: req.user._id,
|
||||
workspaceId: integration.workspace,
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@ const requireIntegrationAuthorizationAuth = ({
|
||||
}
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: integrationAuth.workspace._id.toString(),
|
||||
userId: req.user._id,
|
||||
workspaceId: integrationAuth.workspace._id,
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ const requireMembershipAuth = ({
|
||||
if (!userMembership) throw new Error('Failed to validate own membership')
|
||||
|
||||
const targetMembership = await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: membership.workspace.toString(),
|
||||
userId: req.user._id,
|
||||
workspaceId: membership.workspace,
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UnauthorizedRequestError } from '../utils/errors';
|
||||
import {
|
||||
MembershipOrg
|
||||
} from '../models';
|
||||
import { validateMembership } from '../helpers/membershipOrg';
|
||||
import { validateMembershipOrg } from '../helpers/membershipOrg';
|
||||
|
||||
|
||||
type req = 'params' | 'body' | 'query';
|
||||
@@ -17,9 +17,11 @@ type req = 'params' | 'body' | 'query';
|
||||
*/
|
||||
const requireMembershipOrgAuth = ({
|
||||
acceptedRoles,
|
||||
acceptedStatuses,
|
||||
location = 'params'
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
acceptedStatuses: string[];
|
||||
location?: req;
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -29,14 +31,13 @@ const requireMembershipOrgAuth = ({
|
||||
|
||||
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 = await validateMembershipOrg({
|
||||
userId: req.user._id,
|
||||
organizationId: membershipOrg.organization,
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
|
||||
req.targetMembership = targetMembership;
|
||||
|
||||
return next();
|
||||
} catch (err) {
|
||||
return next(UnauthorizedRequestError({
|
||||
|
||||
@@ -1,44 +1,35 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import { IOrganization, MembershipOrg } from '../models';
|
||||
import { UnauthorizedRequestError, ValidationError } from '../utils/errors';
|
||||
import { validateMembershipOrg } from '../helpers/membershipOrg';
|
||||
|
||||
type req = 'params' | 'body' | 'query';
|
||||
|
||||
/**
|
||||
* Validate if user on request is a member with proper roles for organization
|
||||
* on request params.
|
||||
* @param {Object} obj
|
||||
* @param {String[]} obj.acceptedRoles - accepted organization roles
|
||||
* @param {String[]} obj.acceptedStatuses - accepted organization statuses
|
||||
* @param {String[]} obj.accepteStatuses - accepted organization statuses
|
||||
*/
|
||||
const requireOrganizationAuth = ({
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
acceptedStatuses,
|
||||
location = 'params'
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
acceptedStatuses: string[];
|
||||
location?: req;
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
// organization authorization middleware
|
||||
|
||||
// validate organization membership
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: req.user._id,
|
||||
organization: req.params.organizationId
|
||||
}).populate<{ organization: IOrganization }>('organization');
|
||||
|
||||
|
||||
if (!membershipOrg) {
|
||||
return next(UnauthorizedRequestError({message: "You're not a member of this Organization."}))
|
||||
}
|
||||
//TODO is this important to validate? I mean is it possible to save wrong role to database or get wrong role from databse? - Zamion101
|
||||
if (!acceptedRoles.includes(membershipOrg.role)) {
|
||||
return next(ValidationError({message: 'Failed to validate Organization Membership Role'}))
|
||||
}
|
||||
|
||||
if (!acceptedStatuses.includes(membershipOrg.status)) {
|
||||
return next(ValidationError({message: 'Failed to validate Organization Membership Status'}))
|
||||
}
|
||||
|
||||
req.membershipOrg = membershipOrg;
|
||||
const { organizationId } = req[location];
|
||||
req.membershipOrg = await validateMembershipOrg({
|
||||
userId: req.user._id,
|
||||
organizationId: new Types.ObjectId(organizationId),
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
@@ -32,8 +32,8 @@ const requireSecretAuth = ({
|
||||
}
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: secret.workspace.toString(),
|
||||
userId: req.user._id,
|
||||
workspaceId: secret.workspace,
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
|
||||
@@ -1,48 +1,34 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { UnauthorizedRequestError } from '../utils/errors';
|
||||
import { Secret, Membership } from '../models';
|
||||
import { validateSecrets } from '../helpers/secret';
|
||||
|
||||
// TODO: make this work for delete route
|
||||
import { validateClientForSecrets } from '../helpers/secrets';
|
||||
|
||||
const requireSecretsAuth = ({
|
||||
acceptedRoles
|
||||
acceptedRoles,
|
||||
requiredPermissions = []
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
let secrets;
|
||||
try {
|
||||
if (Array.isArray(req.body.secrets)) {
|
||||
// case: validate multiple secrets
|
||||
secrets = await validateSecrets({
|
||||
userId: req.user._id.toString(),
|
||||
secretIds: req.body.secrets.map((s: any) => s.id)
|
||||
});
|
||||
} else if (typeof req.body.secrets === 'object') { // change this to check for object
|
||||
// case: validate 1 secret
|
||||
secrets = await validateSecrets({
|
||||
userId: req.user._id.toString(),
|
||||
secretIds: [req.body.secrets.id]
|
||||
});
|
||||
} else if (Array.isArray(req.body.secretIds)) {
|
||||
secrets = await validateSecrets({
|
||||
userId: req.user._id.toString(),
|
||||
secretIds: req.body.secretIds
|
||||
});
|
||||
} else if (typeof req.body.secretIds === 'string') {
|
||||
// case: validate secretIds
|
||||
secrets = await validateSecrets({
|
||||
userId: req.user._id.toString(),
|
||||
secretIds: [req.body.secretIds]
|
||||
});
|
||||
}
|
||||
|
||||
req.secrets = secrets;
|
||||
return next();
|
||||
} catch (err) {
|
||||
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret(s)' }));
|
||||
let secretIds = [];
|
||||
if (Array.isArray(req.body.secrets)) {
|
||||
secretIds = req.body.secrets.map((s: any) => s.id);
|
||||
} else if (typeof req.body.secrets === 'object') {
|
||||
secretIds = [req.body.secrets.id];
|
||||
} else if (Array.isArray(req.body.secretIds)) {
|
||||
secretIds = req.body.secretIds;
|
||||
} else if (typeof req.body.secretIds === 'string') {
|
||||
secretIds = [req.body.secretIds];
|
||||
}
|
||||
|
||||
req.secrets = await validateClientForSecrets({
|
||||
authData: req.authData,
|
||||
secretIds: [req.body.secretIds],
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
40
backend/src/middleware/requireServiceAccountAuth.ts
Normal file
40
backend/src/middleware/requireServiceAccountAuth.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import { ServiceAccount } from '../models';
|
||||
import {
|
||||
ServiceAccountNotFoundError
|
||||
} from '../utils/errors';
|
||||
import {
|
||||
validateMembershipOrg
|
||||
} from '../helpers/membershipOrg';
|
||||
import {
|
||||
validateClientForServiceAccount
|
||||
} from '../helpers/serviceAccount';
|
||||
|
||||
type req = 'params' | 'body' | 'query';
|
||||
|
||||
const requireServiceAccountAuth = ({
|
||||
acceptedRoles,
|
||||
acceptedStatuses,
|
||||
locationServiceAccountId = 'params',
|
||||
requiredPermissions = []
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
acceptedStatuses: string[];
|
||||
locationServiceAccountId?: req;
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const serviceAccountId = req[locationServiceAccountId].serviceAccountId;
|
||||
|
||||
req.serviceAccount = await validateClientForServiceAccount({
|
||||
authData: req.authData,
|
||||
serviceAccountId: new Types.ObjectId(serviceAccountId),
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
export default requireServiceAccountAuth;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ServiceAccount, ServiceAccountWorkspacePermission } from '../models';
|
||||
import {
|
||||
ServiceAccountNotFoundError
|
||||
} from '../utils/errors';
|
||||
import {
|
||||
validateMembershipOrg
|
||||
} from '../helpers/membershipOrg';
|
||||
|
||||
type req = 'params' | 'body' | 'query';
|
||||
|
||||
const requireServiceAccountWorkspacePermissionAuth = ({
|
||||
acceptedRoles,
|
||||
acceptedStatuses,
|
||||
location = 'params'
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
acceptedStatuses: string[];
|
||||
location?: req;
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const serviceAccountWorkspacePermissionId = req[location].serviceAccountWorkspacePermissionId;
|
||||
const serviceAccountWorkspacePermission = await ServiceAccountWorkspacePermission.findById(serviceAccountWorkspacePermissionId);
|
||||
|
||||
if (!serviceAccountWorkspacePermission) {
|
||||
return next(ServiceAccountNotFoundError({ message: 'Failed to locate Service Account workspace permission' }));
|
||||
}
|
||||
|
||||
const serviceAccount = await ServiceAccount.findById(serviceAccountWorkspacePermission.serviceAccount);
|
||||
|
||||
if (!serviceAccount) {
|
||||
return next(ServiceAccountNotFoundError({ message: 'Failed to locate Service Account' }));
|
||||
}
|
||||
|
||||
if (serviceAccount.user.toString() !== req.user.id.toString()) {
|
||||
// case: creator of the service account is different from
|
||||
// the user on the request -> apply middleware role/status validation
|
||||
await validateMembershipOrg({
|
||||
userId: req.user._id,
|
||||
organizationId: serviceAccount.organization,
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
}
|
||||
|
||||
req.serviceAccount = serviceAccount;
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
export default requireServiceAccountWorkspacePermissionAuth;
|
||||
@@ -26,8 +26,8 @@ const requireServiceTokenDataAuth = ({
|
||||
if (req.user) {
|
||||
// case: jwt auth
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: serviceTokenData.workspace.toString(),
|
||||
userId: req.user._id,
|
||||
workspaceId: serviceTokenData.workspace,
|
||||
acceptedRoles
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import { validateMembership } from '../helpers/membership';
|
||||
import { validateClientForWorkspace } from '../helpers/workspace';
|
||||
import { UnauthorizedRequestError } from '../utils/errors';
|
||||
|
||||
type req = 'params' | 'body' | 'query';
|
||||
@@ -13,38 +15,33 @@ type req = 'params' | 'body' | 'query';
|
||||
*/
|
||||
const requireWorkspaceAuth = ({
|
||||
acceptedRoles,
|
||||
location = 'params'
|
||||
locationWorkspaceId,
|
||||
locationEnvironment = undefined,
|
||||
requiredPermissions = []
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
location?: req;
|
||||
locationWorkspaceId: req;
|
||||
locationEnvironment?: req | undefined;
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { workspaceId } = req[location];
|
||||
|
||||
if (req.user) {
|
||||
// case: jwt auth
|
||||
const membership = await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId,
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
req.membership = membership;
|
||||
}
|
||||
|
||||
if (
|
||||
req.serviceTokenData
|
||||
&& req.serviceTokenData.workspace.toString() !== workspaceId
|
||||
&& req.serviceTokenData.environment !== req.body.environment
|
||||
) {
|
||||
next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (err) {
|
||||
return next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
|
||||
|
||||
const workspaceId = req[locationWorkspaceId]?.workspaceId;
|
||||
const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined;
|
||||
|
||||
// validate clients
|
||||
const { membership } = await validateClientForWorkspace({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
if (membership) {
|
||||
req.membership = membership;
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Schema, model, Types } from 'mongoose';
|
||||
export interface IAPIKeyData {
|
||||
name: string;
|
||||
user: Types.ObjectId;
|
||||
lastUsed: Date;
|
||||
expiresAt: Date;
|
||||
secretHash: string;
|
||||
}
|
||||
@@ -18,6 +19,9 @@ const apiKeyDataSchema = new Schema<IAPIKeyData>(
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
lastUsed: {
|
||||
type: Date
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date
|
||||
},
|
||||
|
||||
@@ -10,6 +10,10 @@ import MembershipOrg, { IMembershipOrg } from './membershipOrg';
|
||||
import Organization, { IOrganization } from './organization';
|
||||
import Secret, { ISecret } from './secret';
|
||||
import ServiceToken, { IServiceToken } from './serviceToken';
|
||||
import ServiceAccount, { IServiceAccount } from './serviceAccount'; // new
|
||||
import ServiceAccountKey, { IServiceAccountKey } from './serviceAccountKey'; // new
|
||||
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from './serviceAccountOrganizationPermission'; // new
|
||||
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from './serviceAccountWorkspacePermission'; // new
|
||||
import TokenData, { ITokenData } from './tokenData';
|
||||
import User, { IUser } from './user';
|
||||
import UserAction, { IUserAction } from './userAction';
|
||||
@@ -43,6 +47,14 @@ export {
|
||||
ISecret,
|
||||
ServiceToken,
|
||||
IServiceToken,
|
||||
ServiceAccount,
|
||||
IServiceAccount,
|
||||
ServiceAccountKey,
|
||||
IServiceAccountKey,
|
||||
ServiceAccountOrganizationPermission,
|
||||
IServiceAccountOrganizationPermission,
|
||||
ServiceAccountWorkspacePermission,
|
||||
IServiceAccountWorkspacePermission,
|
||||
TokenData,
|
||||
ITokenData,
|
||||
User,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_RAILWAY,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_TRAVISCI,
|
||||
@@ -20,9 +21,12 @@ export interface IIntegration {
|
||||
environment: string;
|
||||
isActive: boolean;
|
||||
app: string;
|
||||
appId: string;
|
||||
owner: string;
|
||||
targetEnvironment: string;
|
||||
appId: string;
|
||||
targetEnvironmentId: string;
|
||||
targetService: string;
|
||||
targetServiceId: string;
|
||||
path: string;
|
||||
region: string;
|
||||
integration:
|
||||
@@ -35,6 +39,7 @@ export interface IIntegration {
|
||||
| 'github'
|
||||
| 'gitlab'
|
||||
| 'render'
|
||||
| 'railway'
|
||||
| 'flyio'
|
||||
| 'circleci'
|
||||
| 'travisci';
|
||||
@@ -71,6 +76,20 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
targetEnvironmentId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
targetService: {
|
||||
// railway-specific service
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
targetServiceId: {
|
||||
// railway-specific service
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
owner: {
|
||||
// github-specific repo owner-login
|
||||
type: String,
|
||||
@@ -99,6 +118,7 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_RAILWAY,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_TRAVISCI,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_RAILWAY,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_TRAVISCI,
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
export interface IIntegrationAuth {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'aws-parameter-store' | 'aws-secret-manager';
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'aws-parameter-store' | 'aws-secret-manager';
|
||||
teamId: string;
|
||||
accountId: string;
|
||||
refreshCiphertext?: string;
|
||||
@@ -51,6 +52,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_RAILWAY,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_TRAVISCI,
|
||||
|
||||
53
backend/src/models/serviceAccount.ts
Normal file
53
backend/src/models/serviceAccount.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Schema, model, Types, Document } from 'mongoose';
|
||||
|
||||
export interface IServiceAccount extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
organization: Types.ObjectId;
|
||||
user: Types.ObjectId;
|
||||
publicKey: string;
|
||||
lastUsed: Date;
|
||||
expiresAt: Date;
|
||||
secretHash: string;
|
||||
}
|
||||
|
||||
const serviceAccountSchema = new Schema<IServiceAccount>(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Organization',
|
||||
required: true
|
||||
},
|
||||
user: { // user who created the service account
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
publicKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
lastUsed: {
|
||||
type: Date
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date
|
||||
},
|
||||
secretHash: {
|
||||
type: String,
|
||||
required: true,
|
||||
select: false
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const ServiceAccount = model<IServiceAccount>('ServiceAccount', serviceAccountSchema);
|
||||
|
||||
export default ServiceAccount;
|
||||
44
backend/src/models/serviceAccountKey.ts
Normal file
44
backend/src/models/serviceAccountKey.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
|
||||
export interface IServiceAccountKey {
|
||||
_id: Types.ObjectId;
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
sender: Types.ObjectId;
|
||||
serviceAccount: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
}
|
||||
|
||||
const serviceAccountKeySchema = new Schema<IServiceAccountKey>(
|
||||
{
|
||||
encryptedKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
nonce: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
sender: {
|
||||
type: Schema.Types.ObjectId,
|
||||
required: true
|
||||
},
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceAccount',
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const ServiceAccountKey = model<IServiceAccountKey>('ServiceAccountKey', serviceAccountKeySchema);
|
||||
|
||||
export default ServiceAccountKey;
|
||||
23
backend/src/models/serviceAccountOrganizationPermission.ts
Normal file
23
backend/src/models/serviceAccountOrganizationPermission.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Schema, model, Types, Document } from 'mongoose';
|
||||
|
||||
export interface IServiceAccountOrganizationPermission extends Document {
|
||||
_id: Types.ObjectId;
|
||||
serviceAccount: Types.ObjectId;
|
||||
}
|
||||
|
||||
const serviceAccountOrganizationPermissionSchema = new Schema<IServiceAccountOrganizationPermission>(
|
||||
{
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceAccount',
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const ServiceAccountOrganizationPermission = model<IServiceAccountOrganizationPermission>('ServiceAccountOrganizationPermission', serviceAccountOrganizationPermissionSchema);
|
||||
|
||||
export default ServiceAccountOrganizationPermission;
|
||||
44
backend/src/models/serviceAccountWorkspacePermission.ts
Normal file
44
backend/src/models/serviceAccountWorkspacePermission.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Schema, model, Types, Document } from 'mongoose';
|
||||
|
||||
export interface IServiceAccountWorkspacePermission extends Document {
|
||||
_id: Types.ObjectId;
|
||||
serviceAccount: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
environment: string;
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
}
|
||||
|
||||
const serviceAccountWorkspacePermissionSchema = new Schema<IServiceAccountWorkspacePermission>(
|
||||
{
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceAccount',
|
||||
required: true
|
||||
},
|
||||
workspace:{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
read: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
write: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const ServiceAccountWorkspacePermission = model<IServiceAccountWorkspacePermission>('ServiceAccountWorkspacePermission', serviceAccountWorkspacePermissionSchema);
|
||||
|
||||
export default ServiceAccountWorkspacePermission;
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import { Schema, model, Types, Document } from 'mongoose';
|
||||
|
||||
export interface IServiceTokenData {
|
||||
export interface IServiceTokenData extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
workspace: Types.ObjectId;
|
||||
environment: string;
|
||||
user: Types.ObjectId;
|
||||
serviceAccount: Types.ObjectId;
|
||||
lastUsed: Date;
|
||||
expiresAt: Date;
|
||||
secretHash: string;
|
||||
encryptedKey: string;
|
||||
@@ -24,14 +27,20 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
environment: { // TODO: adapt to upcoming environment id
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
ref: 'User'
|
||||
},
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceAccount'
|
||||
},
|
||||
lastUsed: {
|
||||
type: Date
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date
|
||||
|
||||
@@ -4,6 +4,7 @@ import { body } from 'express-validator';
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { authController } from '../../controllers/v1';
|
||||
import { authLimiter } from '../../helpers/rateLimiter';
|
||||
import { AUTH_MODE_JWT } from '../../variables';
|
||||
|
||||
router.post('/token', validateRequest, authController.getNewToken);
|
||||
|
||||
@@ -29,7 +30,7 @@ router.post(
|
||||
'/logout',
|
||||
authLimiter,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
authController.logout
|
||||
);
|
||||
@@ -37,7 +38,7 @@ router.post(
|
||||
router.post(
|
||||
'/checkAuth',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
authController.checkAuth
|
||||
);
|
||||
|
||||
@@ -8,15 +8,16 @@ import {
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { botController } from '../../controllers/v1';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import { ADMIN, MEMBER, AUTH_MODE_JWT } from '../../variables';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
@@ -26,7 +27,7 @@ router.get(
|
||||
router.patch(
|
||||
'/:botId/active',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireBotAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
|
||||
@@ -6,14 +6,19 @@ import {
|
||||
requireIntegrationAuthorizationAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../../variables';
|
||||
import { body, param } from 'express-validator';
|
||||
import { integrationController } from '../../controllers/v1';
|
||||
|
||||
router.post( // new: add new integration for integration auth
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -25,6 +30,9 @@ router.post( // new: add new integration for integration auth
|
||||
body('appId').trim(),
|
||||
body('sourceEnvironment').trim(),
|
||||
body('targetEnvironment').trim(),
|
||||
body('targetEnvironmentId').trim(),
|
||||
body('targetService').trim(),
|
||||
body('targetServiceId').trim(),
|
||||
body('owner').trim(),
|
||||
body('path').trim(),
|
||||
body('region').trim(),
|
||||
@@ -35,7 +43,7 @@ router.post( // new: add new integration for integration auth
|
||||
router.patch(
|
||||
'/:integrationId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireIntegrationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@@ -54,7 +62,7 @@ router.patch(
|
||||
router.delete(
|
||||
'/:integrationId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireIntegrationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
|
||||
@@ -7,13 +7,18 @@ import {
|
||||
requireIntegrationAuthorizationAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../../variables';
|
||||
import { integrationAuthController } from '../../controllers/v1';
|
||||
|
||||
router.get(
|
||||
'/integration-options',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
integrationAuthController.getIntegrationOptions
|
||||
);
|
||||
@@ -21,7 +26,7 @@ router.get(
|
||||
router.get(
|
||||
'/:integrationAuthId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@@ -34,11 +39,11 @@ router.get(
|
||||
router.post(
|
||||
'/oauth-token',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
locationWorkspaceId: 'body'
|
||||
}),
|
||||
body('workspaceId').exists().trim().notEmpty(),
|
||||
body('code').exists().trim().notEmpty(),
|
||||
@@ -49,25 +54,25 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/access-token',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
}),
|
||||
body('workspaceId').exists().trim().notEmpty(),
|
||||
body('accessId').trim(),
|
||||
body('accessToken').exists().trim().notEmpty(),
|
||||
body('integration').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'body'
|
||||
}),
|
||||
integrationAuthController.saveIntegrationAccessToken
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:integrationAuthId/apps',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@@ -81,7 +86,7 @@ router.get(
|
||||
router.get(
|
||||
'/:integrationAuthId/teams',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@@ -106,10 +111,38 @@ router.get(
|
||||
integrationAuthController.getIntegrationAuthVercelBranches
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:integrationAuthId/railway/environments',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('integrationAuthId').exists().isString(),
|
||||
query('appId').exists().isString(),
|
||||
validateRequest,
|
||||
integrationAuthController.getIntegrationAuthRailwayEnvironments
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:integrationAuthId/railway/services',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('integrationAuthId').exists().isString(),
|
||||
query('appId').exists().isString(),
|
||||
validateRequest,
|
||||
integrationAuthController.getIntegrationAuthRailwayServices
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:integrationAuthId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
|
||||
@@ -3,11 +3,12 @@ const router = express.Router();
|
||||
import { body } from 'express-validator';
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { membershipOrgController } from '../../controllers/v1';
|
||||
import { AUTH_MODE_JWT } from '../../variables';
|
||||
|
||||
router.post(
|
||||
'/signup',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
body('inviteeEmail').exists().trim().notEmpty().isEmail(),
|
||||
body('organizationId').exists().trim().notEmpty(),
|
||||
|
||||
@@ -6,16 +6,17 @@ import {
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { body, param } from 'express-validator';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import { ADMIN, MEMBER, AUTH_MODE_JWT } from '../../variables';
|
||||
import { keyController } from '../../controllers/v1';
|
||||
|
||||
router.post(
|
||||
'/:workspaceId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('key').exists(),
|
||||
@@ -26,10 +27,11 @@ router.post(
|
||||
router.get(
|
||||
'/:workspaceId/latest',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId'),
|
||||
validateRequest,
|
||||
|
||||
@@ -4,13 +4,14 @@ import { body, param } from 'express-validator';
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { membershipController } from '../../controllers/v1';
|
||||
import { membershipController as EEMembershipControllers } from '../../ee/controllers/v1';
|
||||
import { AUTH_MODE_JWT } from '../../variables';
|
||||
|
||||
// note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId)
|
||||
|
||||
router.get( // used for old CLI (deprecate)
|
||||
'/:workspaceId/connect',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
@@ -20,7 +21,7 @@ router.get( // used for old CLI (deprecate)
|
||||
router.delete(
|
||||
'/:membershipId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
param('membershipId').exists().trim(),
|
||||
validateRequest,
|
||||
@@ -30,7 +31,7 @@ router.delete(
|
||||
router.post(
|
||||
'/:membershipId/change-role',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
body('role').exists().trim(),
|
||||
validateRequest,
|
||||
@@ -40,7 +41,7 @@ router.post(
|
||||
router.post(
|
||||
'/:membershipId/deny-permissions',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
param('membershipId').isMongoId().exists().trim(),
|
||||
body('permissions').isArray().exists(),
|
||||
|
||||
@@ -3,12 +3,13 @@ const router = express.Router();
|
||||
import { param } from 'express-validator';
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { membershipOrgController } from '../../controllers/v1';
|
||||
import { AUTH_MODE_JWT } from '../../variables';
|
||||
|
||||
router.post(
|
||||
// TODO
|
||||
'/membershipOrg/:membershipOrgId/change-role',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
param('membershipOrgId'),
|
||||
validateRequest,
|
||||
@@ -18,7 +19,7 @@ router.post(
|
||||
router.delete(
|
||||
'/:membershipOrgId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
param('membershipOrgId').exists().trim(),
|
||||
validateRequest,
|
||||
|
||||
@@ -6,13 +6,19 @@ import {
|
||||
requireOrganizationAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables';
|
||||
import {
|
||||
OWNER,
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
ACCEPTED,
|
||||
AUTH_MODE_JWT
|
||||
} from '../../variables';
|
||||
import { organizationController } from '../../controllers/v1';
|
||||
|
||||
router.get( // deprecated (moved to api/v2/users/me/organizations)
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
organizationController.getOrganizations
|
||||
);
|
||||
@@ -20,7 +26,7 @@ router.get( // deprecated (moved to api/v2/users/me/organizations)
|
||||
router.post( // not used on frontend
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
body('organizationName').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
@@ -30,7 +36,7 @@ router.post( // not used on frontend
|
||||
router.get(
|
||||
'/:organizationId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
@@ -44,7 +50,7 @@ router.get(
|
||||
router.get( // deprecated (moved to api/v2/organizations/:organizationId/memberships)
|
||||
'/:organizationId/users',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
@@ -58,7 +64,7 @@ router.get( // deprecated (moved to api/v2/organizations/:organizationId/members
|
||||
router.get(
|
||||
'/:organizationId/my-workspaces', // deprecated (moved to api/v2/organizations/:organizationId/workspaces)
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
@@ -72,7 +78,7 @@ router.get(
|
||||
router.patch(
|
||||
'/:organizationId/name',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
@@ -87,7 +93,7 @@ router.patch(
|
||||
router.get(
|
||||
'/:organizationId/incidentContactOrg',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
@@ -101,7 +107,7 @@ router.get(
|
||||
router.post(
|
||||
'/:organizationId/incidentContactOrg',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
@@ -116,7 +122,7 @@ router.post(
|
||||
router.delete(
|
||||
'/:organizationId/incidentContactOrg',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
@@ -131,7 +137,7 @@ router.delete(
|
||||
router.post(
|
||||
'/:organizationId/customer-portal-session',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
@@ -145,7 +151,7 @@ router.post(
|
||||
router.get(
|
||||
'/:organizationId/subscriptions',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
@@ -159,7 +165,7 @@ router.get(
|
||||
router.get(
|
||||
'/:organizationId/workspace-memberships',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
|
||||
@@ -4,11 +4,14 @@ import { body } from 'express-validator';
|
||||
import { requireAuth, requireSignupAuth, validateRequest } from '../../middleware';
|
||||
import { passwordController } from '../../controllers/v1';
|
||||
import { passwordLimiter } from '../../helpers/rateLimiter';
|
||||
import {
|
||||
AUTH_MODE_JWT
|
||||
} from '../../variables';
|
||||
|
||||
router.post(
|
||||
'/srp1',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
body('clientPublicKey').exists().isString().trim().notEmpty(),
|
||||
validateRequest,
|
||||
@@ -19,7 +22,7 @@ router.post(
|
||||
'/change-password',
|
||||
passwordLimiter,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
body('clientProof').exists().trim().notEmpty(),
|
||||
body('protectedKey').exists().isString().trim().notEmpty(),
|
||||
@@ -62,7 +65,7 @@ router.post(
|
||||
'/backup-private-key',
|
||||
passwordLimiter,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
body('clientProof').exists().isString().trim().notEmpty(),
|
||||
body('encryptedPrivateKey').exists().isString().trim().notEmpty(), // (backup) private key encrypted under a strong key
|
||||
|
||||
@@ -8,15 +8,22 @@ import {
|
||||
} from '../../middleware';
|
||||
import { body, query, param } from 'express-validator';
|
||||
import { secretController } from '../../controllers/v1';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
AUTH_MODE_JWT
|
||||
} from '../../variables';
|
||||
|
||||
// note to devs: these endpoints will be deprecated in favor of v2
|
||||
|
||||
router.post(
|
||||
'/:workspaceId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
body('secrets').exists(),
|
||||
body('keys').exists(),
|
||||
@@ -30,10 +37,11 @@ router.post(
|
||||
router.get(
|
||||
'/:workspaceId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
query('environment').exists().trim(),
|
||||
query('channel'),
|
||||
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { body } from 'express-validator';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
AUTH_MODE_JWT
|
||||
} from '../../variables';
|
||||
import { serviceTokenController } from '../../controllers/v1';
|
||||
|
||||
// note: deprecate service-token routes in favor of service-token data routes/structure
|
||||
@@ -21,11 +25,11 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
locationWorkspaceId: 'body'
|
||||
}),
|
||||
body('name').exists().trim().notEmpty(),
|
||||
body('workspaceId').exists().trim().notEmpty(),
|
||||
|
||||
@@ -2,11 +2,14 @@ import express from 'express';
|
||||
const router = express.Router();
|
||||
import { requireAuth } from '../../middleware';
|
||||
import { userController } from '../../controllers/v1';
|
||||
import {
|
||||
AUTH_MODE_JWT
|
||||
} from '../../variables';
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
userController.getUser
|
||||
);
|
||||
|
||||
@@ -3,12 +3,13 @@ const router = express.Router();
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { body, query } from 'express-validator';
|
||||
import { userActionController } from '../../controllers/v1';
|
||||
import { AUTH_MODE_JWT } from '../../variables';
|
||||
|
||||
// note: [userAction] will be deprecated in /v2 in favor of [action]
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
body('action'),
|
||||
validateRequest,
|
||||
@@ -18,7 +19,7 @@ router.post(
|
||||
router.get(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
query('action'),
|
||||
validateRequest,
|
||||
|
||||
@@ -6,16 +6,21 @@ import {
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
AUTH_MODE_JWT
|
||||
} from '../../variables';
|
||||
import { workspaceController, membershipController } from '../../controllers/v1';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/keys',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
@@ -25,10 +30,11 @@ router.get(
|
||||
router.get(
|
||||
'/:workspaceId/users',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
@@ -38,7 +44,7 @@ router.get(
|
||||
router.get(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
workspaceController.getWorkspaces
|
||||
);
|
||||
@@ -46,10 +52,11 @@ router.get(
|
||||
router.get(
|
||||
'/:workspaceId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
@@ -59,7 +66,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
body('workspaceName').exists().trim().notEmpty(),
|
||||
body('organizationId').exists().trim().notEmpty(),
|
||||
@@ -70,10 +77,11 @@ router.post(
|
||||
router.delete(
|
||||
'/:workspaceId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN]
|
||||
acceptedRoles: [ADMIN],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
@@ -83,10 +91,11 @@ router.delete(
|
||||
router.post(
|
||||
'/:workspaceId/name',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('name').exists().trim().notEmpty(),
|
||||
@@ -97,10 +106,11 @@ router.post(
|
||||
router.post(
|
||||
'/:workspaceId/invite-signup',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('email').exists().trim().notEmpty(),
|
||||
@@ -111,10 +121,11 @@ router.post(
|
||||
router.get(
|
||||
'/:workspaceId/integrations',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
@@ -124,10 +135,11 @@ router.get(
|
||||
router.get(
|
||||
'/:workspaceId/authorizations',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
@@ -137,10 +149,11 @@ router.get(
|
||||
router.get(
|
||||
'/:workspaceId/service-tokens', // deprecate
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { param, body } from 'express-validator';
|
||||
import {
|
||||
requireAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { param, body } from 'express-validator';
|
||||
import { apiKeyDataController } from '../../controllers/v2';
|
||||
import {
|
||||
AUTH_MODE_JWT
|
||||
} from '../../variables';
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
apiKeyDataController.getAPIKeyData
|
||||
);
|
||||
@@ -18,7 +21,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
body('name').exists().trim(),
|
||||
body('expiresIn'), // measured in ms
|
||||
@@ -29,7 +32,7 @@ router.post(
|
||||
router.delete(
|
||||
'/:apiKeyDataId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
param('apiKeyDataId').exists().trim(),
|
||||
validateRequest,
|
||||
|
||||
@@ -7,15 +7,20 @@ import {
|
||||
requireWorkspaceAuth,
|
||||
validateRequest,
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
AUTH_MODE_JWT
|
||||
} from '../../variables';
|
||||
|
||||
router.post(
|
||||
'/:workspaceId/environments',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt'],
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('environmentSlug').exists().trim(),
|
||||
@@ -27,10 +32,11 @@ router.post(
|
||||
router.put(
|
||||
'/:workspaceId/environments',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt'],
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('environmentSlug').exists().trim(),
|
||||
@@ -43,10 +49,11 @@ router.put(
|
||||
router.delete(
|
||||
'/:workspaceId/environments',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt'],
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('environmentSlug').exists().trim(),
|
||||
@@ -57,14 +64,15 @@ router.delete(
|
||||
router.get(
|
||||
'/:workspaceId/environments',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt'],
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [MEMBER, ADMIN],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
environmentController.getAllAccessibleEnvironmentsOfWorkspace
|
||||
);
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
@@ -6,6 +6,7 @@ import workspace from './workspace';
|
||||
import secret from './secret'; // deprecated
|
||||
import secrets from './secrets';
|
||||
import serviceTokenData from './serviceTokenData';
|
||||
import serviceAccounts from './serviceAccounts';
|
||||
import apiKeyData from './apiKeyData';
|
||||
import environment from "./environment"
|
||||
import tags from "./tags"
|
||||
@@ -19,6 +20,7 @@ export {
|
||||
secret,
|
||||
secrets,
|
||||
serviceTokenData,
|
||||
serviceAccounts,
|
||||
apiKeyData,
|
||||
environment,
|
||||
tags
|
||||
|
||||
@@ -6,8 +6,15 @@ import {
|
||||
requireMembershipOrgAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { body, param, query } from 'express-validator';
|
||||
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables';
|
||||
import { body, param } from 'express-validator';
|
||||
import {
|
||||
OWNER,
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
ACCEPTED,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../../variables';
|
||||
import { organizationsController } from '../../controllers/v2';
|
||||
|
||||
// TODO: /POST to create membership
|
||||
@@ -17,7 +24,7 @@ router.get(
|
||||
param('organizationId').exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
@@ -33,14 +40,15 @@ router.patch(
|
||||
body('role').exists().isString().trim().isIn([OWNER, ADMIN, MEMBER]),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
requireMembershipOrgAuth({
|
||||
acceptedRoles: [OWNER, ADMIN]
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
organizationsController.updateOrganizationMembership
|
||||
);
|
||||
@@ -51,14 +59,15 @@ router.delete(
|
||||
param('membershipId').exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
requireMembershipOrgAuth({
|
||||
acceptedRoles: [OWNER, ADMIN]
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
organizationsController.deleteOrganizationMembership
|
||||
);
|
||||
@@ -68,7 +77,7 @@ router.get(
|
||||
param('organizationId').exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
@@ -77,4 +86,18 @@ router.get(
|
||||
organizationsController.getOrganizationWorkspaces
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:organizationId/service-accounts',
|
||||
param('organizationId').exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
organizationsController.getOrganizationServiceAccounts
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -6,7 +6,12 @@ import {
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { body, param, query } from 'express-validator';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_TOKEN
|
||||
} from '../../variables';
|
||||
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
|
||||
import { secretController } from '../../controllers/v2';
|
||||
|
||||
@@ -17,10 +22,11 @@ const router = express.Router();
|
||||
router.post(
|
||||
'/batch-create/workspace/:workspaceId/environment/:environment',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().isMongoId().trim(),
|
||||
param('environment').exists().trim(),
|
||||
@@ -33,10 +39,11 @@ router.post(
|
||||
router.post(
|
||||
'/workspace/:workspaceId/environment/:environment',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().isMongoId().trim(),
|
||||
param('environment').exists().trim(),
|
||||
@@ -51,10 +58,11 @@ router.get(
|
||||
param('workspaceId').exists().trim(),
|
||||
query("environment").exists(),
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'serviceToken']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
query('channel'),
|
||||
validateRequest,
|
||||
@@ -64,7 +72,7 @@ router.get(
|
||||
router.get(
|
||||
'/:secretId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'serviceToken']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
requireSecretAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@@ -76,13 +84,14 @@ router.get(
|
||||
router.delete(
|
||||
'/batch/workspace/:workspaceId/environment/:environmentName',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
param('workspaceId').exists().isMongoId().trim(),
|
||||
param('environmentName').exists().trim(),
|
||||
body('secretIds').exists().isArray().custom(array => array.length > 0),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
validateRequest,
|
||||
secretController.deleteSecrets
|
||||
@@ -91,7 +100,7 @@ router.delete(
|
||||
router.delete(
|
||||
'/:secretId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireSecretAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@@ -104,29 +113,30 @@ router.delete(
|
||||
router.patch(
|
||||
'/batch-modify/workspace/:workspaceId/environment/:environmentName',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
body('secrets').exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0),
|
||||
param('workspaceId').exists().isMongoId().trim(),
|
||||
param('environmentName').exists().trim(),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
validateRequest,
|
||||
secretController.updateSecrets
|
||||
);
|
||||
|
||||
|
||||
router.patch(
|
||||
'/workspace/:workspaceId/environment/:environmentName',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
body('secret').isObject(),
|
||||
param('workspaceId').exists().isMongoId().trim(),
|
||||
param('environmentName').exists().trim(),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
validateRequest,
|
||||
secretController.updateSecret
|
||||
|
||||
@@ -8,12 +8,18 @@ import {
|
||||
} from '../../middleware';
|
||||
import { query, body } from 'express-validator';
|
||||
import { secretsController } from '../../controllers/v2';
|
||||
import { validateSecrets } from '../../helpers/secret';
|
||||
import { validateClientForSecrets } from '../../helpers/secrets';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED
|
||||
SECRET_SHARED,
|
||||
PERMISSION_READ_SECRETS,
|
||||
PERMISSION_WRITE_SECRETS,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../../variables';
|
||||
import {
|
||||
BatchSecretRequest
|
||||
@@ -22,12 +28,11 @@ import {
|
||||
router.post(
|
||||
'/batch',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['read', 'write']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
locationWorkspaceId: 'body'
|
||||
}),
|
||||
body('workspaceId').exists().isString().trim(),
|
||||
body('environment').exists().isString().trim(),
|
||||
@@ -40,12 +45,11 @@ router.post(
|
||||
.filter((secretId) => secretId !== undefined)
|
||||
|
||||
if (secretIds.length > 0) {
|
||||
const relevantSecrets = await validateSecrets({
|
||||
userId: req.user._id.toString(),
|
||||
secretIds
|
||||
req.secrets = await validateClientForSecrets({
|
||||
authData: req.authData,
|
||||
secretIds,
|
||||
requiredPermissions: []
|
||||
});
|
||||
|
||||
req.secrets = relevantSecrets;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -100,12 +104,13 @@ router.post(
|
||||
}),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['write']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
locationWorkspaceId: 'body',
|
||||
locationEnvironment: 'body',
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
}),
|
||||
secretsController.createSecrets
|
||||
);
|
||||
@@ -117,12 +122,13 @@ router.get(
|
||||
query('tagSlugs'),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['read']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'query'
|
||||
locationWorkspaceId: 'query',
|
||||
locationEnvironment: 'query',
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS]
|
||||
}),
|
||||
secretsController.getSecrets
|
||||
);
|
||||
@@ -157,11 +163,11 @@ router.patch(
|
||||
}),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['write']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
requireSecretsAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
}),
|
||||
secretsController.updateSecrets
|
||||
);
|
||||
@@ -186,14 +192,13 @@ router.delete(
|
||||
.isEmpty(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
|
||||
requiredServiceTokenPermissions: ['write']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
requireSecretsAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
}),
|
||||
secretsController.deleteSecrets
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
export default router;
|
||||
159
backend/src/routes/v2/serviceAccounts.ts
Normal file
159
backend/src/routes/v2/serviceAccounts.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireOrganizationAuth,
|
||||
requireWorkspaceAuth,
|
||||
requireServiceAccountAuth,
|
||||
requireServiceAccountWorkspacePermissionAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { param, query, body } from 'express-validator';
|
||||
import {
|
||||
OWNER,
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
ACCEPTED,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
} from '../../variables';
|
||||
import { serviceAccountsController } from '../../controllers/v2';
|
||||
|
||||
router.get( // TODO: check
|
||||
'/me',
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_SERVICE_ACCOUNT]
|
||||
}),
|
||||
serviceAccountsController.getCurrentServiceAccount
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:serviceAccountId',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.getServiceAccountById
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
body('organizationId').exists().isString().trim(),
|
||||
body('name').exists().isString().trim(),
|
||||
body('publicKey').exists().isString().trim(),
|
||||
body('expiresIn').isNumeric(), // measured in ms
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
location: 'body'
|
||||
}),
|
||||
serviceAccountsController.createServiceAccount
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:serviceAccountId/name',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.changeServiceAccountName
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:serviceAccountId',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.deleteServiceAccount
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:serviceAccountId/permissions/workspace',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.getServiceAccountWorkspacePermissions
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:serviceAccountId/permissions/workspace',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
body('workspaceId').exists().isString().notEmpty(),
|
||||
body('environment').exists().isString().notEmpty(),
|
||||
body('read').isBoolean().optional(),
|
||||
body('write').isBoolean().optional(),
|
||||
body('encryptedKey').exists().isString().notEmpty(),
|
||||
body('nonce').exists().isString().notEmpty(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'body'
|
||||
}),
|
||||
serviceAccountsController.addServiceAccountWorkspacePermission
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:serviceAccountId/permissions/workspace/:serviceAccountWorkspacePermissionId',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
param('serviceAccountWorkspacePermissionId').exists().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
requireServiceAccountWorkspacePermissionAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.deleteServiceAccountWorkspacePermission
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:serviceAccountId/keys',
|
||||
query('workspaceId').optional().isString(),
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT]
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.getServiceAccountKeys
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -10,13 +10,17 @@ import { param, body } from 'express-validator';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
PERMISSION_WRITE_SECRETS,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN
|
||||
} from '../../variables';
|
||||
import { serviceTokenDataController } from '../../controllers/v2';
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['serviceToken']
|
||||
acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
serviceTokenDataController.getServiceTokenData
|
||||
);
|
||||
@@ -24,11 +28,13 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
locationWorkspaceId: 'body',
|
||||
locationEnvironment: 'body',
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
}),
|
||||
body('name').exists().isString().trim(),
|
||||
body('workspaceId').exists().isString().trim(),
|
||||
@@ -53,7 +59,7 @@ router.post(
|
||||
router.delete(
|
||||
'/:serviceTokenDataId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireServiceTokenDataAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
|
||||
@@ -5,17 +5,22 @@ import { tagController } from '../../controllers/v2';
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
AUTH_MODE_JWT
|
||||
} from '../../variables';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/tags',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt'],
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [MEMBER, ADMIN],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
@@ -25,7 +30,7 @@ router.get(
|
||||
router.delete(
|
||||
'/tags/:tagId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt'],
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
param('tagId').exists().trim(),
|
||||
validateRequest,
|
||||
@@ -35,10 +40,11 @@ router.delete(
|
||||
router.post(
|
||||
'/:workspaceId/tags',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt'],
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [MEMBER, ADMIN],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('name').exists().trim(),
|
||||
|
||||
@@ -6,11 +6,15 @@ import {
|
||||
} from '../../middleware';
|
||||
import { body } from 'express-validator';
|
||||
import { usersController } from '../../controllers/v2';
|
||||
import {
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../../variables';
|
||||
|
||||
router.get(
|
||||
'/me',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
usersController.getMe
|
||||
);
|
||||
@@ -18,7 +22,7 @@ router.get(
|
||||
router.patch(
|
||||
'/me/mfa',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
body('isMfaEnabled').exists().isBoolean(),
|
||||
validateRequest,
|
||||
@@ -28,7 +32,7 @@ router.patch(
|
||||
router.get(
|
||||
'/me/organizations',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
usersController.getMyOrganizations
|
||||
);
|
||||
|
||||
@@ -7,16 +7,23 @@ import {
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../../variables';
|
||||
import { workspaceController } from '../../controllers/v2';
|
||||
|
||||
router.post(
|
||||
'/:workspaceId/secrets',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
body('secrets').exists(),
|
||||
body('keys').exists(),
|
||||
@@ -30,10 +37,11 @@ router.post(
|
||||
router.get(
|
||||
'/:workspaceId/secrets',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'serviceToken']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
query('environment').exists().trim(),
|
||||
query('channel'),
|
||||
@@ -45,10 +53,11 @@ router.get(
|
||||
router.get(
|
||||
'/:workspaceId/encrypted-key',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
@@ -58,27 +67,27 @@ router.get(
|
||||
router.get(
|
||||
'/:workspaceId/service-token-data',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
workspaceController.getWorkspaceServiceTokenData
|
||||
);
|
||||
|
||||
// TODO: /POST to create membership and re-route inviting user to workspace there
|
||||
|
||||
router.get( // new - TODO: rewire dashboard to this route
|
||||
'/:workspaceId/memberships',
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
workspaceController.getWorkspaceMemberships
|
||||
);
|
||||
@@ -90,10 +99,11 @@ router.patch( // TODO - rewire dashboard to this route
|
||||
body('role').exists().isString().trim().isIn([ADMIN, MEMBER]),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
requireMembershipAuth({
|
||||
acceptedRoles: [ADMIN]
|
||||
@@ -107,10 +117,11 @@ router.delete( // TODO - rewire dashboard to this route
|
||||
param('membershipId').exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
requireMembershipAuth({
|
||||
acceptedRoles: [ADMIN]
|
||||
@@ -121,10 +132,11 @@ router.delete( // TODO - rewire dashboard to this route
|
||||
router.patch(
|
||||
'/:workspaceId/auto-capitalization',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'params'
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('autoCapitalization').exists().trim().notEmpty(),
|
||||
|
||||
8
backend/src/routes/v3/secrets.ts
Normal file
8
backend/src/routes/v3/secrets.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth, validateRequest
|
||||
} from '../../middleware';
|
||||
import { body } from 'express-validator';
|
||||
|
||||
export default router;
|
||||
@@ -1,44 +0,0 @@
|
||||
import { PostHog } from 'posthog-node';
|
||||
import { getLogger } from '../utils/logger';
|
||||
import {
|
||||
getNodeEnv,
|
||||
getTelemetryEnabled,
|
||||
getPostHogProjectApiKey,
|
||||
getPostHogHost
|
||||
} from '../config';
|
||||
|
||||
/**
|
||||
* Logs telemetry enable/disable notice.
|
||||
*/
|
||||
const logTelemetryMessage = () => {
|
||||
if(!getTelemetryEnabled()){
|
||||
getLogger("backend-main").info([
|
||||
"",
|
||||
"To improve, Infisical collects telemetry data about general usage.",
|
||||
"This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.",
|
||||
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
|
||||
].join('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance of the PostHog client initialized.
|
||||
* @returns
|
||||
*/
|
||||
const getPostHogClient = () => {
|
||||
let postHogClient: any;
|
||||
if (getNodeEnv() === 'production' && getTelemetryEnabled()) {
|
||||
// case: enable opt-out telemetry in production
|
||||
postHogClient = new PostHog(getPostHogProjectApiKey(), {
|
||||
host: getPostHogHost()
|
||||
});
|
||||
}
|
||||
|
||||
return postHogClient;
|
||||
}
|
||||
|
||||
export {
|
||||
logTelemetryMessage,
|
||||
getPostHogClient
|
||||
}
|
||||
|
||||
85
backend/src/services/TelemetryService.ts
Normal file
85
backend/src/services/TelemetryService.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { PostHog } from 'posthog-node';
|
||||
import { getLogger } from '../utils/logger';
|
||||
import {
|
||||
getNodeEnv,
|
||||
getTelemetryEnabled,
|
||||
getPostHogProjectApiKey,
|
||||
getPostHogHost
|
||||
} from '../config';
|
||||
import {
|
||||
IUser,
|
||||
IServiceAccount,
|
||||
IServiceTokenData
|
||||
} from '../models';
|
||||
import {
|
||||
BadRequestError
|
||||
} from '../utils/errors';
|
||||
|
||||
class Telemetry {
|
||||
/**
|
||||
* Logs telemetry enable/disable notice.
|
||||
*/
|
||||
static logTelemetryMessage = () => {
|
||||
if(!getTelemetryEnabled()){
|
||||
getLogger("backend-main").info([
|
||||
"",
|
||||
"To improve, Infisical collects telemetry data about general usage.",
|
||||
"This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.",
|
||||
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
|
||||
].join('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance of the PostHog client initialized.
|
||||
* @returns
|
||||
*/
|
||||
static getPostHogClient = () => {
|
||||
let postHogClient: any;
|
||||
if (getNodeEnv() === 'production' && getTelemetryEnabled()) {
|
||||
// case: enable opt-out telemetry in production
|
||||
postHogClient = new PostHog(getPostHogProjectApiKey(), {
|
||||
host: getPostHogHost()
|
||||
});
|
||||
}
|
||||
|
||||
return postHogClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a distinct id for client to be used for logging telemetry
|
||||
*/
|
||||
static getDistinctId = ({
|
||||
user,
|
||||
serviceAccount,
|
||||
serviceTokenData
|
||||
}: {
|
||||
user?: IUser;
|
||||
serviceAccount?: IServiceAccount;
|
||||
serviceTokenData?: IServiceTokenData;
|
||||
}) => {
|
||||
let distinctId = '';
|
||||
|
||||
if (user) {
|
||||
distinctId = user.email;
|
||||
}
|
||||
|
||||
if (serviceAccount) {
|
||||
distinctId = `sa.${serviceAccount._id}`;
|
||||
}
|
||||
|
||||
if (serviceTokenData) {
|
||||
distinctId = `st.${serviceTokenData._id}`;
|
||||
}
|
||||
|
||||
if (distinctId === '') {
|
||||
throw BadRequestError({
|
||||
message: 'Failed to obtain distinct id for logging telemetry'
|
||||
});
|
||||
}
|
||||
|
||||
return distinctId;
|
||||
}
|
||||
}
|
||||
|
||||
export default Telemetry;
|
||||
@@ -1,13 +1,15 @@
|
||||
import DatabaseService from './DatabaseService';
|
||||
import { logTelemetryMessage, getPostHogClient } from './PostHogClient';
|
||||
// import { logTelemetryMessage, getPostHogClient } from './TelemetryService';
|
||||
import TelemetryService from './TelemetryService';
|
||||
import BotService from './BotService';
|
||||
import EventService from './EventService';
|
||||
import IntegrationService from './IntegrationService';
|
||||
import TokenService from './TokenService';
|
||||
|
||||
export {
|
||||
logTelemetryMessage,
|
||||
getPostHogClient,
|
||||
TelemetryService,
|
||||
// logTelemetryMessage,
|
||||
// getPostHogClient,
|
||||
DatabaseService,
|
||||
BotService,
|
||||
EventService,
|
||||
|
||||
@@ -66,7 +66,7 @@ export const initSmtp = () => {
|
||||
const transporter = nodemailer.createTransport(mailOpts);
|
||||
transporter
|
||||
.verify()
|
||||
.then(() => {
|
||||
.then((err) => {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureMessage('SMTP - Successfully connected');
|
||||
})
|
||||
|
||||
15
backend/src/types/express/index.d.ts
vendored
15
backend/src/types/express/index.d.ts
vendored
@@ -1,5 +1,10 @@
|
||||
import * as express from 'express';
|
||||
import { ISecret } from '../../models';
|
||||
import {
|
||||
IUser,
|
||||
IServiceAccount,
|
||||
IServiceTokenData,
|
||||
ISecret
|
||||
} from '../../models';
|
||||
|
||||
// TODO: fix (any) types
|
||||
declare global {
|
||||
@@ -19,10 +24,18 @@ declare global {
|
||||
secrets: any;
|
||||
secretSnapshot: any;
|
||||
serviceToken: any;
|
||||
serviceAccount: any;
|
||||
accessToken: any;
|
||||
serviceTokenData: any;
|
||||
apiKeyData: any;
|
||||
query?: any;
|
||||
authData: {
|
||||
authMode: string;
|
||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||
};
|
||||
requestData: {
|
||||
[key: string]: string
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,16 @@ export const WorkspaceNotFoundError = (error?: Partial<RequestErrorContext>) =>
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
//* ----->[WORKSPACE MEMBERSHIP ERRORS]<-----
|
||||
export const MembershipNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||
statusCode: error?.statusCode ?? 404,
|
||||
type: error?.type ?? 'workspace_membership_not_found_error',
|
||||
message: error?.message ?? 'The requested membership was not found',
|
||||
context: error?.context,
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
//* ----->[ORGANIZATION ERRORS]<-----
|
||||
export const OrganizationNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||
@@ -103,6 +113,16 @@ export const OrganizationNotFoundError = (error?: Partial<RequestErrorContext>)
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
//* ----->[MEMBERSHIP ORGANIZATION ERRORS]<-----
|
||||
export const MembershipOrgNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||
statusCode: error?.statusCode ?? 404,
|
||||
type: error?.type ?? 'organization_membership_not_found_error',
|
||||
message: error?.message ?? 'The requested organization membership was not found',
|
||||
context: error?.context,
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
//* ----->[ACCOUNT ERRORS]<-----
|
||||
export const AccountNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||
@@ -157,10 +177,29 @@ export const ServiceTokenDataNotFoundError = (error?: Partial<RequestErrorContex
|
||||
export const APIKeyDataNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||
statusCode: error?.statusCode ?? 404,
|
||||
type: error?.type ?? 'service_token_data_not_found_error',
|
||||
type: error?.type ?? 'api_key_data_not_found_error',
|
||||
message: error?.message ?? 'The requested service token data was not found',
|
||||
context: error?.context,
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
//* ----->[SERVICE_ACCOUNT ERRORS]<-----
|
||||
export const ServiceAccountNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||
statusCode: error?.statusCode ?? 404,
|
||||
type: error?.type ?? 'service_account_not_found_error',
|
||||
message: error?.message ?? 'The requested service account was not found',
|
||||
context: error?.context,
|
||||
stack: error?.stack
|
||||
});
|
||||
|
||||
export const ServiceAccountKeyNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||
statusCode: error?.statusCode ?? 404,
|
||||
type: error?.type ?? 'service_account_key_not_found_error',
|
||||
message: error?.message ?? 'The requested service account key was not found',
|
||||
context: error?.context,
|
||||
stack: error?.stack
|
||||
})
|
||||
|
||||
//* ----->[MISC ERRORS]<-----
|
||||
|
||||
11
backend/src/variables/authentication.ts
Normal file
11
backend/src/variables/authentication.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
const AUTH_MODE_JWT = 'jwt';
|
||||
const AUTH_MODE_SERVICE_ACCOUNT = 'serviceAccount';
|
||||
const AUTH_MODE_SERVICE_TOKEN = 'serviceToken';
|
||||
const AUTH_MODE_API_KEY = 'apiKey'; // TODO: deprecate
|
||||
|
||||
export {
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_RAILWAY,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_TRAVISCI,
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
INTEGRATION_RAILWAY_API_URL,
|
||||
INTEGRATION_FLYIO_API_URL,
|
||||
INTEGRATION_CIRCLECI_API_URL,
|
||||
INTEGRATION_TRAVISCI_API_URL,
|
||||
@@ -63,6 +65,16 @@ import {
|
||||
TOKEN_EMAIL_ORG_INVITATION,
|
||||
TOKEN_EMAIL_PASSWORD_RESET
|
||||
} from './token';
|
||||
import {
|
||||
PERMISSION_READ_SECRETS,
|
||||
PERMISSION_WRITE_SECRETS
|
||||
} from './permission';
|
||||
import {
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY
|
||||
} from './authentication';
|
||||
|
||||
export {
|
||||
OWNER,
|
||||
@@ -86,6 +98,7 @@ export {
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_RAILWAY,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_TRAVISCI,
|
||||
@@ -102,6 +115,7 @@ export {
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
INTEGRATION_RAILWAY_API_URL,
|
||||
INTEGRATION_FLYIO_API_URL,
|
||||
INTEGRATION_CIRCLECI_API_URL,
|
||||
INTEGRATION_TRAVISCI_API_URL,
|
||||
@@ -113,6 +127,8 @@ export {
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_DELETE_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
PERMISSION_READ_SECRETS,
|
||||
PERMISSION_WRITE_SECRETS,
|
||||
getIntegrationOptions,
|
||||
SMTP_HOST_SENDGRID,
|
||||
SMTP_HOST_MAILGUN,
|
||||
@@ -124,5 +140,9 @@ export {
|
||||
TOKEN_EMAIL_CONFIRMATION,
|
||||
TOKEN_EMAIL_MFA,
|
||||
TOKEN_EMAIL_ORG_INVITATION,
|
||||
TOKEN_EMAIL_PASSWORD_RESET
|
||||
TOKEN_EMAIL_PASSWORD_RESET,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ const INTEGRATION_NETLIFY = "netlify";
|
||||
const INTEGRATION_GITHUB = "github";
|
||||
const INTEGRATION_GITLAB = "gitlab";
|
||||
const INTEGRATION_RENDER = "render";
|
||||
const INTEGRATION_RAILWAY = "railway";
|
||||
const INTEGRATION_FLYIO = "flyio";
|
||||
const INTEGRATION_CIRCLECI = "circleci";
|
||||
const INTEGRATION_TRAVISCI = "travisci";
|
||||
@@ -52,6 +53,7 @@ const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api";
|
||||
const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
|
||||
const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
|
||||
const INTEGRATION_RENDER_API_URL = "https://api.render.com";
|
||||
const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2";
|
||||
const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
|
||||
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
|
||||
const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
|
||||
@@ -104,6 +106,15 @@ const getIntegrationOptions = () => {
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Railway',
|
||||
slug: 'railway',
|
||||
image: 'Railway.png',
|
||||
isAvailable: true,
|
||||
type: 'pat',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Fly.io',
|
||||
slug: 'flyio',
|
||||
@@ -192,6 +203,7 @@ export {
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_RAILWAY,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_TRAVISCI,
|
||||
@@ -208,6 +220,7 @@ export {
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
INTEGRATION_RAILWAY_API_URL,
|
||||
INTEGRATION_FLYIO_API_URL,
|
||||
INTEGRATION_CIRCLECI_API_URL,
|
||||
INTEGRATION_TRAVISCI_API_URL,
|
||||
|
||||
@@ -6,11 +6,7 @@ const MEMBER = "member";
|
||||
// membership statuses
|
||||
const INVITED = "invited";
|
||||
|
||||
// membership permissions ability
|
||||
const ABILITY_READ = "read";
|
||||
const ABILITY_WRITE = "write";
|
||||
|
||||
// -- organization
|
||||
const ACCEPTED = "accepted";
|
||||
|
||||
export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED, ABILITY_READ, ABILITY_WRITE };
|
||||
export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED };
|
||||
|
||||
7
backend/src/variables/permission.ts
Normal file
7
backend/src/variables/permission.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
const PERMISSION_READ_SECRETS = 'read';
|
||||
const PERMISSION_WRITE_SECRETS = 'write';
|
||||
|
||||
export {
|
||||
PERMISSION_READ_SECRETS,
|
||||
PERMISSION_WRITE_SECRETS
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user