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

View File

@@ -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 {

View File

@@ -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" })
}

View File

@@ -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
});
}

View File

@@ -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

View File

@@ -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" })
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
};

View File

@@ -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'
});
}

View File

@@ -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();
}

View File

@@ -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

View File

@@ -197,5 +197,4 @@ router.delete(
secretsController.deleteSecrets
);
export default router;
export default router;

View File

@@ -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;

View File

@@ -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(),

View File

@@ -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 };

View File

@@ -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>