mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Add required endpoints/functions for service account to create service tokens
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { eventPushSecrets } from '../../events';
|
||||
import { EESecretService, EELogService } from '../../ee/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 _, { eq } from 'lodash';
|
||||
@@ -336,7 +336,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body;
|
||||
|
||||
if (req.user) {
|
||||
const hasAccess = await userHasWorkspaceAccess(req.user, new Types.ObjectId(workspaceId), environment, ABILITY_WRITE)
|
||||
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" })
|
||||
}
|
||||
|
||||
@@ -14,12 +14,29 @@ import {
|
||||
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 getServiceAccount = async (req: Request, res: Response) => {
|
||||
export const getServiceAccountById = async (req: Request, res: Response) => {
|
||||
const { serviceAccountId } = req.params;
|
||||
|
||||
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
|
||||
@@ -136,23 +153,6 @@ export const addServiceAccountKey = async (req: Request, res: Response) => {
|
||||
return serviceAccountKey;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Return organization-level permissions for service account with id [serviceAccountId]
|
||||
// * @param req
|
||||
// * @param res
|
||||
// */
|
||||
// export const getServiceAccountOrganizationPermissions = async (req: Request, res: Response) => {
|
||||
// const { serviceAccountId } = req.params;
|
||||
|
||||
// const permissions = await ServiceAccountOrganizationPermissions.findOne({
|
||||
// serviceAccount: new Types.ObjectId(serviceAccountId),
|
||||
// });
|
||||
|
||||
// return res.status(200).send({
|
||||
// permissions
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* Return workspace-level permission for service account with id [serviceAccountId]
|
||||
* @param req
|
||||
@@ -168,23 +168,6 @@ export const getServiceAccountWorkspacePermissions = async (req: Request, res: R
|
||||
});
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Add organization permissions to service account with id [serviceAccountId]
|
||||
// * @param req
|
||||
// * @param res
|
||||
// */
|
||||
// export const addServiceAccountOrganizationPermission = async (req: Request, res: Response) => {
|
||||
// const permissions = ServiceAccountOrganizationPermissions.findOne({
|
||||
// serviceAccount: req.serviceAccount._id
|
||||
// });
|
||||
|
||||
// // TODO
|
||||
|
||||
// return res.status(200).send({
|
||||
// permissions
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* Add a workspace permission to service account with id [serviceAccountId]
|
||||
* @param req
|
||||
@@ -300,4 +283,23 @@ export const deleteServiceAccount = async (req: Request, res: Response) => {
|
||||
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,
|
||||
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
|
||||
|
||||
@@ -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" })
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => {
|
||||
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||
@@ -26,8 +26,8 @@ export const userHasWriteOnlyAbility = async (userId: Types.ObjectId, workspaceI
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -44,8 +44,8 @@ export const userHasNoAbility = async (userId: Types.ObjectId, workspaceId: Type
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
ACTION_READ_SECRETS
|
||||
} from '../variables';
|
||||
import _ from 'lodash';
|
||||
import { ABILITY_WRITE } from '../variables/organization';
|
||||
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||
|
||||
interface V1PushSecret {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import {
|
||||
validateServiceAccountClientForSecrets
|
||||
} from '../helpers/serviceAccount';
|
||||
import { BadRequestError } from '../utils/errors';
|
||||
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||
import {
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
@@ -60,21 +60,23 @@ const validateClientForSecrets = async ({
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||
// TODO
|
||||
await validateUserClientForSecrets({
|
||||
user: authData.authPayload,
|
||||
secrets,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||
// TODO
|
||||
await validateServiceAccountClientForSecrets({
|
||||
serviceAccount: authData.authPayload,
|
||||
secrets,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||
@@ -83,18 +85,23 @@ const validateClientForSecrets = async ({
|
||||
secrets,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||
// TODO
|
||||
await validateUserClientForSecrets({
|
||||
user: authData.authPayload,
|
||||
secrets,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
return secrets;
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed client authorization for secrets resource'
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
|
||||
@@ -1,17 +1,102 @@
|
||||
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 serviceAccount (client) can access workspace
|
||||
* Validate that service account (client) can access workspace
|
||||
* with id [workspaceId] and its environment [environment] with required permissions
|
||||
* [requiredPermissions]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.
|
||||
* @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,
|
||||
@@ -21,18 +106,51 @@ import {
|
||||
}: {
|
||||
serviceAccount: IServiceAccount;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
requiredPermissions: string[];
|
||||
environment?: string;
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
// TODO
|
||||
return [];
|
||||
// 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 {ServiceTokenData} obj.serviceAccount - service account client
|
||||
* @param {ServiceAccount} obj.serviceAccount - service account client
|
||||
* @param {Secret[]} secrets - secrets to validate against
|
||||
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
|
||||
*/
|
||||
@@ -49,15 +167,73 @@ import {
|
||||
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
|
||||
validateServiceAccountClientForSecrets,
|
||||
validateServiceAccountClientForServiceAccount
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Types } from 'mongoose';
|
||||
import {
|
||||
IUser,
|
||||
ISecret,
|
||||
IServiceAccount,
|
||||
User,
|
||||
Membership
|
||||
} from '../models';
|
||||
@@ -11,8 +12,12 @@ import { validateMembership } from './membership';
|
||||
import _ from 'lodash';
|
||||
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||
import {
|
||||
ABILITY_WRITE
|
||||
} from '../variables/organization';
|
||||
validateMembershipOrg
|
||||
} from '../helpers/membershipOrg';
|
||||
import {
|
||||
PERMISSION_READ_SECRETS,
|
||||
PERMISSION_WRITE_SECRETS
|
||||
} from '../variables';
|
||||
|
||||
/**
|
||||
* Initialize a user under email [email]
|
||||
@@ -180,24 +185,38 @@ const validateUserClientForWorkspace = async ({
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
|
||||
// org-level and workspace-level permissions? - workspace-level env scoped?
|
||||
|
||||
// validate user membership in workspace
|
||||
const membership = await validateMembership({
|
||||
userId: user._id,
|
||||
workspaceId
|
||||
});
|
||||
|
||||
// validate user permission
|
||||
|
||||
|
||||
// TODO: validate that user can perform action on environment in workspace
|
||||
// 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 with ids [secretIds]
|
||||
* Validate that user (client) can access secrets [secrets]
|
||||
* with required permissions [requiredPermissions]
|
||||
* @param {Object} obj
|
||||
* @param {User} obj.user - user client
|
||||
@@ -213,7 +232,7 @@ const validateUserClientForWorkspace = async ({
|
||||
secrets: ISecret[];
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
// TODO: consider refactor
|
||||
// TODO: refactor
|
||||
|
||||
const userMemberships = await Membership.find({ user: user._id })
|
||||
const userMembershipById = _.keyBy(userMemberships, 'workspace');
|
||||
@@ -221,27 +240,59 @@ const validateUserClientForWorkspace = async ({
|
||||
|
||||
// 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())) {
|
||||
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: ABILITY_WRITE });
|
||||
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'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw BadRequestError({
|
||||
message: 'You cannot edit secrets of a workspace you are not a member of'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
validateUserClientForSecrets,
|
||||
validateUserClientForServiceAccount
|
||||
};
|
||||
|
||||
@@ -53,30 +53,26 @@ const validateClientForWorkspace = async ({
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
|
||||
let membership;
|
||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||
membership = await validateUserClientForWorkspace({
|
||||
const membership = await validateUserClientForWorkspace({
|
||||
user: authData.authPayload,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
// TODO: validate user against [requiredPermissions]
|
||||
return ({ membership });
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||
const permission = await ServiceAccountWorkspacePermission.findOne({
|
||||
serviceAccount: authData.authPayload._id,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment
|
||||
await validateServiceAccountClientForWorkspace({
|
||||
serviceAccount: authData.authPayload,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
if (!permission) throw UnauthorizedRequestError({
|
||||
message: 'Failed service account authorization for the given workspace environment'
|
||||
});
|
||||
|
||||
// TODO: validate [requiredPermissions] against [permission]
|
||||
return {};
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||
@@ -87,20 +83,22 @@ const validateClientForWorkspace = async ({
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
// TODO: validate [requiredPermissions] against [permission]
|
||||
return {};
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||
membership = await validateUserClientForWorkspace({
|
||||
const membership = await validateUserClientForWorkspace({
|
||||
user: authData.authPayload,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return ({ membership });
|
||||
}
|
||||
|
||||
return ({
|
||||
membership
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed client authorization for workspace resource'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import { ServiceAccount } from '../models';
|
||||
import {
|
||||
ServiceAccountNotFoundError
|
||||
@@ -6,38 +7,31 @@ import {
|
||||
import {
|
||||
validateMembershipOrg
|
||||
} from '../helpers/membershipOrg';
|
||||
import {
|
||||
validateClientForServiceAccount
|
||||
} from '../helpers/serviceAccount';
|
||||
|
||||
type req = 'params' | 'body' | 'query';
|
||||
|
||||
const requireServiceAccountAuth = ({
|
||||
acceptedRoles,
|
||||
acceptedStatuses,
|
||||
location = 'params'
|
||||
locationServiceAccountId = 'params',
|
||||
requiredPermissions = []
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
acceptedStatuses: string[];
|
||||
location?: req;
|
||||
locationServiceAccountId?: req;
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const serviceAccountId = req[location].serviceAccountId;
|
||||
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
|
||||
const serviceAccountId = req[locationServiceAccountId].serviceAccountId;
|
||||
|
||||
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;
|
||||
req.serviceAccount = await validateClientForServiceAccount({
|
||||
authData: req.authData,
|
||||
serviceAccountId: new Types.ObjectId(serviceAccountId),
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface IServiceTokenData extends Document {
|
||||
workspace: Types.ObjectId;
|
||||
environment: string;
|
||||
user: Types.ObjectId;
|
||||
serviceAccount: Types.ObjectId;
|
||||
expiresAt: Date;
|
||||
secretHash: string;
|
||||
encryptedKey: string;
|
||||
@@ -31,8 +32,11 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
ref: 'User'
|
||||
},
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'ServiceAccount'
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date
|
||||
|
||||
@@ -197,5 +197,4 @@ router.delete(
|
||||
secretsController.deleteSecrets
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
export default router;
|
||||
@@ -17,6 +17,14 @@ import {
|
||||
} from '../../variables';
|
||||
import { serviceAccountsController } from '../../controllers/v2';
|
||||
|
||||
router.get( // TODO: check
|
||||
'/me',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['serviceAccount']
|
||||
}),
|
||||
serviceAccountsController.getCurrentServiceAccount
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:serviceAccountId',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
@@ -27,7 +35,7 @@ router.get(
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.getServiceAccount
|
||||
serviceAccountsController.getServiceAccountById
|
||||
);
|
||||
|
||||
router.post(
|
||||
@@ -76,22 +84,6 @@ router.delete(
|
||||
serviceAccountsController.deleteServiceAccount
|
||||
);
|
||||
|
||||
// router.get(
|
||||
// '/:serviceAccountId/permissions/organization',
|
||||
// param('serviceAccountId').exists().isString().trim(),
|
||||
// query('offset').exists(),
|
||||
// query('limit').exists(),
|
||||
// validateRequest,
|
||||
// requireAuth({
|
||||
// acceptedAuthModes: ['jwt']
|
||||
// }),
|
||||
// requireServiceAccountAuth({
|
||||
// acceptedRoles: [OWNER, ADMIN],
|
||||
// acceptedStatuses: [ACCEPTED]
|
||||
// }),
|
||||
// serviceAccountsController.getServiceAccountOrganizationPermission
|
||||
// );
|
||||
|
||||
router.get(
|
||||
'/:serviceAccountId/permissions/workspace',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
@@ -106,20 +98,6 @@ router.get(
|
||||
serviceAccountsController.getServiceAccountWorkspacePermissions
|
||||
);
|
||||
|
||||
// router.post(
|
||||
// '/:serviceAccountId/permissions/organization',
|
||||
// param('serviceAccountId').exists().isString().trim(),
|
||||
// validateRequest,
|
||||
// requireAuth({
|
||||
// acceptedAuthModes: ['jwt']
|
||||
// }),
|
||||
// requireServiceAccountAuth({
|
||||
// acceptedRoles: [OWNER, ADMIN],
|
||||
// acceptedStatuses: [ACCEPTED]
|
||||
// }),
|
||||
// serviceAccountsController.addServiceAccountOrganizationPermission
|
||||
// );
|
||||
|
||||
router.post(
|
||||
'/:serviceAccountId/permissions/workspace',
|
||||
param('serviceAccountId').exists().isString().trim(),
|
||||
@@ -163,16 +141,17 @@ router.delete(
|
||||
serviceAccountsController.deleteServiceAccountWorkspacePermission
|
||||
);
|
||||
|
||||
// router.post(
|
||||
// '/:serviceAccountId/key',
|
||||
// body('workspaceId').exists().isString().trim(),
|
||||
// body('encryptedKey').exists().isString().trim(),
|
||||
// body('nonce').exists().isString().trim(),
|
||||
// requireServiceAccountAuth({
|
||||
// acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
// acceptedStatuses: [ACCEPTED]
|
||||
// }),
|
||||
// serviceAccountsController.addServiceAccountKey
|
||||
// );
|
||||
router.get(
|
||||
'/:serviceAccountId/keys',
|
||||
query('workspaceId').optional().isString(),
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'serviceAccount']
|
||||
}),
|
||||
requireServiceAccountAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
serviceAccountsController.getServiceAccountKeys
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -10,6 +10,7 @@ import { param, body } from 'express-validator';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
PERMISSION_WRITE_SECRETS
|
||||
} from '../../variables';
|
||||
import { serviceTokenDataController } from '../../controllers/v2';
|
||||
|
||||
@@ -24,11 +25,13 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: ['jwt', 'serviceAccount']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: 'body'
|
||||
locationWorkspaceId: 'body',
|
||||
locationEnvironment: 'body',
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
}),
|
||||
body('name').exists().isString().trim(),
|
||||
body('workspaceId').exists().isString().trim(),
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -117,7 +117,7 @@ export const SAProjectLevelPermissionsTable = ({
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey: serviceAccount.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
privateKey
|
||||
});
|
||||
|
||||
await createServiceAccountProjectLevelPermission.mutateAsync({
|
||||
@@ -171,8 +171,6 @@ export const SAProjectLevelPermissionsTable = ({
|
||||
<Th>Environment</Th>
|
||||
<Th>Read</Th>
|
||||
<Th>Write</Th>
|
||||
<Th>Update</Th>
|
||||
<Th>Delete</Th>
|
||||
<Th aria-label="actions" />
|
||||
</Tr>
|
||||
</THead>
|
||||
|
||||
Reference in New Issue
Block a user