Add required endpoints/functions for service account to create service tokens

This commit is contained in:
Tuan Dang
2023-04-06 16:59:55 +03:00
parent a3b2d1c838
commit d54753289a
18 changed files with 431 additions and 220 deletions
@@ -11,7 +11,7 @@ import {
import { SecretVersion } from '../../ee/models'; import { SecretVersion } from '../../ee/models';
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from '../../utils/errors';
import _ from 'lodash'; 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 * Create new workspace environment named [environmentName] under workspace with id
@@ -244,8 +244,8 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (
throw BadRequestError() throw BadRequestError()
} }
relatedWorkspace.environments.forEach(environment => { relatedWorkspace.environments.forEach(environment => {
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ }) const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS })
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE }) const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS })
if (isReadBlocked && isWriteBlocked) { if (isReadBlocked && isWriteBlocked) {
return return
} else { } else {
@@ -17,7 +17,7 @@ import { eventPushSecrets } from '../../events';
import { EESecretService, EELogService } from '../../ee/services'; import { EESecretService, EELogService } from '../../ee/services';
import { TelemetryService } from '../../services'; import { TelemetryService } from '../../services';
import { getChannelFromUserAgent } from '../../utils/posthog'; 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 { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
import Tag from '../../models/tag'; import Tag from '../../models/tag';
import _, { eq } from 'lodash'; 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; const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body;
if (req.user) { 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) { if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) 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 { BadRequestError, ServiceAccountNotFoundError } from '../../utils/errors';
import { getSaltRounds } from '../../config'; 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] * Return service account with id [serviceAccountId]
* @param req * @param req
* @param res * @param res
*/ */
export const getServiceAccount = async (req: Request, res: Response) => { export const getServiceAccountById = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params; const { serviceAccountId } = req.params;
const serviceAccount = await ServiceAccount.findById(serviceAccountId); const serviceAccount = await ServiceAccount.findById(serviceAccountId);
@@ -136,23 +153,6 @@ export const addServiceAccountKey = async (req: Request, res: Response) => {
return serviceAccountKey; 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] * Return workspace-level permission for service account with id [serviceAccountId]
* @param req * @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] * Add a workspace permission to service account with id [serviceAccountId]
* @param req * @param req
@@ -301,3 +284,22 @@ export const deleteServiceAccount = async (req: Request, res: Response) => {
serviceAccount 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 crypto from 'crypto';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { import {
User,
ServiceAccount,
ServiceTokenData ServiceTokenData
} from '../../models'; } from '../../models';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; 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'; import { getSaltRounds } from '../../config';
/** /**
@@ -53,59 +59,57 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createServiceTokenData = async (req: Request, res: Response) => { export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceToken, serviceTokenData; let serviceTokenData;
try { const {
const { name,
name, workspaceId,
workspaceId, environment,
environment, encryptedKey,
encryptedKey, iv,
iv, tag,
tag, expiresIn,
expiresIn, permissions
permissions } = req.body;
} = req.body;
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ) const secret = crypto.randomBytes(16).toString('hex');
if (!hasAccess) { const secretHash = await bcrypt.hash(secret, getSaltRounds());
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
const secret = crypto.randomBytes(16).toString('hex'); const expiresAt = new Date();
const secretHash = await bcrypt.hash(secret, getSaltRounds()); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
const expiresAt = new Date(); let user, serviceAccount;
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
serviceTokenData = await new ServiceTokenData({ if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
name, user = req.authData.authPayload._id;
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'
});
} }
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({ return res.status(200).send({
serviceToken, serviceToken,
serviceTokenData serviceTokenData
@@ -2,7 +2,8 @@ import { Request, Response } from "express";
import { Membership, Workspace } from "../../../models"; import { Membership, Workspace } from "../../../models";
import { IMembershipPermission } from "../../../models/membership"; import { IMembershipPermission } from "../../../models/membership";
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors"; 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 { Builder } from "builder-pattern"
import _ from "lodash"; import _ from "lodash";
@@ -10,7 +11,7 @@ export const denyMembershipPermissions = async (req: Request, res: Response) =>
const { membershipId } = req.params; const { membershipId } = req.params;
const { permissions } = req.body; const { permissions } = req.body;
const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => { 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" }) 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 { Types } from 'mongoose';
import _ from "lodash"; import _ from "lodash";
import { Membership } from "../../models"; 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) => { export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) 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 deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
// case: you have write only if read is blocked and write is not // case: you have write only if read is blocked and write is not
if (isReadDisallowed && !isWriteDisallowed) { if (isReadDisallowed && !isWriteDisallowed) {
@@ -44,8 +44,8 @@ export const userHasNoAbility = async (userId: Types.ObjectId, workspaceId: Type
} }
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
if (isReadBlocked && isWriteDisallowed) { if (isReadBlocked && isWriteDisallowed) {
return true return true
-1
View File
@@ -21,7 +21,6 @@ import {
ACTION_READ_SECRETS ACTION_READ_SECRETS
} from '../variables'; } from '../variables';
import _ from 'lodash'; import _ from 'lodash';
import { ABILITY_WRITE } from '../variables/organization';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
interface V1PushSecret { interface V1PushSecret {
+12 -5
View File
@@ -18,7 +18,7 @@ import {
import { import {
validateServiceAccountClientForSecrets validateServiceAccountClientForSecrets
} from '../helpers/serviceAccount'; } from '../helpers/serviceAccount';
import { BadRequestError } from '../utils/errors'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import { import {
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT,
@@ -60,21 +60,23 @@ const validateClientForSecrets = async ({
} }
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
// TODO
await validateUserClientForSecrets({ await validateUserClientForSecrets({
user: authData.authPayload, user: authData.authPayload,
secrets, secrets,
requiredPermissions requiredPermissions
}); });
return secrets;
} }
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
// TODO
await validateServiceAccountClientForSecrets({ await validateServiceAccountClientForSecrets({
serviceAccount: authData.authPayload, serviceAccount: authData.authPayload,
secrets, secrets,
requiredPermissions requiredPermissions
}); });
return secrets;
} }
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
@@ -83,18 +85,23 @@ const validateClientForSecrets = async ({
secrets, secrets,
requiredPermissions requiredPermissions
}); });
return secrets;
} }
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
// TODO
await validateUserClientForSecrets({ await validateUserClientForSecrets({
user: authData.authPayload, user: authData.authPayload,
secrets, secrets,
requiredPermissions requiredPermissions
}); });
return secrets;
} }
return secrets; throw UnauthorizedRequestError({
message: 'Failed client authorization for secrets resource'
});
} }
export { export {
+185 -9
View File
@@ -1,17 +1,102 @@
import _ from 'lodash'; import _ from 'lodash';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
User,
IUser,
ServiceAccount,
IServiceAccount, IServiceAccount,
ServiceTokenData,
IServiceTokenData,
ISecret, ISecret,
ServiceAccountWorkspacePermission ServiceAccountWorkspacePermission
} from '../models'; } 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 * with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions] * [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 ({ const validateServiceAccountClientForWorkspace = async ({
serviceAccount, serviceAccount,
@@ -21,18 +106,51 @@ import {
}: { }: {
serviceAccount: IServiceAccount; serviceAccount: IServiceAccount;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment?: string;
requiredPermissions: string[]; requiredPermissions?: string[];
}) => { }) => {
// TODO // TODO: add service account API support for workspace-level endpoints that are not
return []; // 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 * Validate that service account (client) can access secrets
* with required permissions [requiredPermissions] * with required permissions [requiredPermissions]
* @param {Object} obj * @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 {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint * @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/ */
@@ -49,15 +167,73 @@ import {
const permissions = await ServiceAccountWorkspacePermission.find({ const permissions = await ServiceAccountWorkspacePermission.find({
serviceAccount: serviceAccount._id serviceAccount: serviceAccount._id
}); });
const permissionsObj = _.keyBy(permissions, (p) => { const permissionsObj = _.keyBy(permissions, (p) => {
return `${p.workspace.toString()}-${p.environment}` 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 // TODO
return []; 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 { export {
validateClientForServiceAccount,
validateServiceAccountClientForWorkspace, validateServiceAccountClientForWorkspace,
validateServiceAccountClientForSecrets validateServiceAccountClientForSecrets,
validateServiceAccountClientForServiceAccount
} }
+67 -16
View File
@@ -3,6 +3,7 @@ import { Types } from 'mongoose';
import { import {
IUser, IUser,
ISecret, ISecret,
IServiceAccount,
User, User,
Membership Membership
} from '../models'; } from '../models';
@@ -11,8 +12,12 @@ import { validateMembership } from './membership';
import _ from 'lodash'; import _ from 'lodash';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import { import {
ABILITY_WRITE validateMembershipOrg
} from '../variables/organization'; } from '../helpers/membershipOrg';
import {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../variables';
/** /**
* Initialize a user under email [email] * Initialize a user under email [email]
@@ -180,24 +185,38 @@ const validateUserClientForWorkspace = async ({
requiredPermissions?: string[]; requiredPermissions?: string[];
}) => { }) => {
// org-level and workspace-level permissions? - workspace-level env scoped?
// validate user membership in workspace // validate user membership in workspace
const membership = await validateMembership({ const membership = await validateMembership({
userId: user._id, userId: user._id,
workspaceId workspaceId
}); });
// validate user permission // 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) {
// TODO: validate that user can perform action on environment in workspace throw UnauthorizedRequestError({
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
});
}
});
return membership; return membership;
} }
/** /**
* Validate that user (client) can access secrets with ids [secretIds] * Validate that user (client) can access secrets [secrets]
* with required permissions [requiredPermissions] * with required permissions [requiredPermissions]
* @param {Object} obj * @param {Object} obj
* @param {User} obj.user - user client * @param {User} obj.user - user client
@@ -213,7 +232,7 @@ const validateUserClientForWorkspace = async ({
secrets: ISecret[]; secrets: ISecret[];
requiredPermissions?: string[]; requiredPermissions?: string[];
}) => { }) => {
// TODO: consider refactor // TODO: refactor
const userMemberships = await Membership.find({ user: user._id }) const userMemberships = await Membership.find({ user: user._id })
const userMembershipById = _.keyBy(userMemberships, 'workspace'); 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 // for each secret check if the secret belongs to a workspace the user is a member of
secrets.forEach((secret: ISecret) => { 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 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) { if (isDisallowed) {
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: 'You do not have the required permissions to perform this action' 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 { export {
setupAccount, setupAccount,
completeAccount, completeAccount,
checkUserDevice, checkUserDevice,
validateUserClientForWorkspace, validateUserClientForWorkspace,
validateUserClientForSecrets validateUserClientForSecrets,
validateUserClientForServiceAccount
}; };
+14 -16
View File
@@ -53,30 +53,26 @@ const validateClientForWorkspace = async ({
requiredPermissions?: string[]; requiredPermissions?: string[];
}) => { }) => {
let membership;
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
membership = await validateUserClientForWorkspace({ const membership = await validateUserClientForWorkspace({
user: authData.authPayload, user: authData.authPayload,
workspaceId, workspaceId,
environment, environment,
requiredPermissions requiredPermissions
}); });
// TODO: validate user against [requiredPermissions] return ({ membership });
} }
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
const permission = await ServiceAccountWorkspacePermission.findOne({ await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload._id, serviceAccount: authData.authPayload,
workspace: new Types.ObjectId(workspaceId), workspaceId,
environment environment,
requiredPermissions
}); });
if (!permission) throw UnauthorizedRequestError({ return {};
message: 'Failed service account authorization for the given workspace environment'
});
// TODO: validate [requiredPermissions] against [permission]
} }
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
@@ -87,20 +83,22 @@ const validateClientForWorkspace = async ({
requiredPermissions requiredPermissions
}); });
// TODO: validate [requiredPermissions] against [permission] return {};
} }
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
membership = await validateUserClientForWorkspace({ const membership = await validateUserClientForWorkspace({
user: authData.authPayload, user: authData.authPayload,
workspaceId, workspaceId,
environment, environment,
requiredPermissions requiredPermissions
}); });
return ({ membership });
} }
return ({ throw UnauthorizedRequestError({
membership message: 'Failed client authorization for workspace resource'
}); });
} }
@@ -1,4 +1,5 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { ServiceAccount } from '../models'; import { ServiceAccount } from '../models';
import { import {
ServiceAccountNotFoundError ServiceAccountNotFoundError
@@ -6,38 +7,31 @@ import {
import { import {
validateMembershipOrg validateMembershipOrg
} from '../helpers/membershipOrg'; } from '../helpers/membershipOrg';
import {
validateClientForServiceAccount
} from '../helpers/serviceAccount';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
const requireServiceAccountAuth = ({ const requireServiceAccountAuth = ({
acceptedRoles, acceptedRoles,
acceptedStatuses, acceptedStatuses,
location = 'params' locationServiceAccountId = 'params',
requiredPermissions = []
}: { }: {
acceptedRoles: string[]; acceptedRoles: string[];
acceptedStatuses: string[]; acceptedStatuses: string[];
location?: req; locationServiceAccountId?: req;
requiredPermissions?: string[];
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
const serviceAccountId = req[location].serviceAccountId; const serviceAccountId = req[locationServiceAccountId].serviceAccountId;
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
if (!serviceAccount) { req.serviceAccount = await validateClientForServiceAccount({
return next(ServiceAccountNotFoundError({ message: 'Failed to locate Service Account' })); authData: req.authData,
} serviceAccountId: new Types.ObjectId(serviceAccountId),
requiredPermissions
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(); next();
} }
+6 -2
View File
@@ -6,6 +6,7 @@ export interface IServiceTokenData extends Document {
workspace: Types.ObjectId; workspace: Types.ObjectId;
environment: string; environment: string;
user: Types.ObjectId; user: Types.ObjectId;
serviceAccount: Types.ObjectId;
expiresAt: Date; expiresAt: Date;
secretHash: string; secretHash: string;
encryptedKey: string; encryptedKey: string;
@@ -31,8 +32,11 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
}, },
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'User', ref: 'User'
required: true },
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
}, },
expiresAt: { expiresAt: {
type: Date type: Date
-1
View File
@@ -198,4 +198,3 @@ router.delete(
); );
export default router; export default router;
+21 -42
View File
@@ -17,6 +17,14 @@ import {
} from '../../variables'; } from '../../variables';
import { serviceAccountsController } from '../../controllers/v2'; import { serviceAccountsController } from '../../controllers/v2';
router.get( // TODO: check
'/me',
requireAuth({
acceptedAuthModes: ['serviceAccount']
}),
serviceAccountsController.getCurrentServiceAccount
);
router.get( router.get(
'/:serviceAccountId', '/:serviceAccountId',
param('serviceAccountId').exists().isString().trim(), param('serviceAccountId').exists().isString().trim(),
@@ -27,7 +35,7 @@ router.get(
acceptedRoles: [OWNER, ADMIN], acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
}), }),
serviceAccountsController.getServiceAccount serviceAccountsController.getServiceAccountById
); );
router.post( router.post(
@@ -76,22 +84,6 @@ router.delete(
serviceAccountsController.deleteServiceAccount 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( router.get(
'/:serviceAccountId/permissions/workspace', '/:serviceAccountId/permissions/workspace',
param('serviceAccountId').exists().isString().trim(), param('serviceAccountId').exists().isString().trim(),
@@ -106,20 +98,6 @@ router.get(
serviceAccountsController.getServiceAccountWorkspacePermissions 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( router.post(
'/:serviceAccountId/permissions/workspace', '/:serviceAccountId/permissions/workspace',
param('serviceAccountId').exists().isString().trim(), param('serviceAccountId').exists().isString().trim(),
@@ -163,16 +141,17 @@ router.delete(
serviceAccountsController.deleteServiceAccountWorkspacePermission serviceAccountsController.deleteServiceAccountWorkspacePermission
); );
// router.post( router.get(
// '/:serviceAccountId/key', '/:serviceAccountId/keys',
// body('workspaceId').exists().isString().trim(), query('workspaceId').optional().isString(),
// body('encryptedKey').exists().isString().trim(), requireAuth({
// body('nonce').exists().isString().trim(), acceptedAuthModes: ['jwt', 'serviceAccount']
// requireServiceAccountAuth({ }),
// acceptedRoles: [OWNER, ADMIN, MEMBER], requireServiceAccountAuth({
// acceptedStatuses: [ACCEPTED] acceptedRoles: [OWNER, ADMIN],
// }), acceptedStatuses: [ACCEPTED]
// serviceAccountsController.addServiceAccountKey }),
// ); serviceAccountsController.getServiceAccountKeys
);
export default router; export default router;
+5 -2
View File
@@ -10,6 +10,7 @@ import { param, body } from 'express-validator';
import { import {
ADMIN, ADMIN,
MEMBER, MEMBER,
PERMISSION_WRITE_SECRETS
} from '../../variables'; } from '../../variables';
import { serviceTokenDataController } from '../../controllers/v2'; import { serviceTokenDataController } from '../../controllers/v2';
@@ -24,11 +25,13 @@ router.get(
router.post( router.post(
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt', 'serviceAccount']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body' locationWorkspaceId: 'body',
locationEnvironment: 'body',
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
body('name').exists().isString().trim(), body('name').exists().isString().trim(),
body('workspaceId').exists().isString().trim(), body('workspaceId').exists().isString().trim(),
+1 -5
View File
@@ -6,11 +6,7 @@ const MEMBER = "member";
// membership statuses // membership statuses
const INVITED = "invited"; const INVITED = "invited";
// membership permissions ability
const ABILITY_READ = "read";
const ABILITY_WRITE = "write";
// -- organization // -- organization
const ACCEPTED = "accepted"; 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({ const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key, plaintext: key,
publicKey: serviceAccount.publicKey, publicKey: serviceAccount.publicKey,
privateKey: PRIVATE_KEY privateKey
}); });
await createServiceAccountProjectLevelPermission.mutateAsync({ await createServiceAccountProjectLevelPermission.mutateAsync({
@@ -171,8 +171,6 @@ export const SAProjectLevelPermissionsTable = ({
<Th>Environment</Th> <Th>Environment</Th>
<Th>Read</Th> <Th>Read</Th>
<Th>Write</Th> <Th>Write</Th>
<Th>Update</Th>
<Th>Delete</Th>
<Th aria-label="actions" /> <Th aria-label="actions" />
</Tr> </Tr>
</THead> </THead>