Begin refactoring middleware for service accounts

This commit is contained in:
Tuan Dang
2023-04-04 11:08:03 +03:00
parent c797901778
commit aa53de9070
39 changed files with 943 additions and 577 deletions
+16 -11
View File
@@ -22,6 +22,12 @@ import {
getJwtRefreshLifetime,
getJwtRefreshSecret
} from '../config';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
*
@@ -39,7 +45,7 @@ const validateAuthMode = ({
const apiKey = headers['x-api-key'];
const authHeader = headers['authorization'];
let authTokenType, authTokenValue;
let authMode, authTokenValue;
if (apiKey === undefined && authHeader === undefined) {
// case: no auth or X-API-KEY header present
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
@@ -47,7 +53,7 @@ const validateAuthMode = ({
if (typeof apiKey === 'string') {
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
authTokenType = 'apiKey';
authMode = AUTH_MODE_API_KEY;
authTokenValue = apiKey;
}
@@ -63,24 +69,24 @@ const validateAuthMode = ({
switch (tokenValue.split('.', 1)[0]) {
case 'st':
authTokenType = 'serviceToken';
authMode = AUTH_MODE_SERVICE_TOKEN;
break;
case 'sa':
authTokenType = 'serviceAccount';
authMode = AUTH_MODE_SERVICE_ACCOUNT;
break;
default:
authTokenType = 'jwt';
authMode = AUTH_MODE_JWT;
}
authTokenValue = tokenValue;
}
if (!authTokenType || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
if (!authMode || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
if (!acceptedAuthModes.includes(authTokenType)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
if (!acceptedAuthModes.includes(authMode)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
return ({
authTokenType,
authMode,
authTokenValue
});
}
@@ -155,8 +161,7 @@ const getAuthSTDPayload = async ({
serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER)
.select('+encryptedKey +iv +tag')
.populate<{user: IUser}>('user');
.select('+encryptedKey +iv +tag');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
@@ -216,7 +221,7 @@ const getAuthAPIKeyPayload = async ({
const apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate('user', '+publicKey');
.populate<{user: IUser}>('user', '+publicKey');
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
-52
View File
@@ -24,57 +24,6 @@ import _ from 'lodash';
import { ABILITY_WRITE } from '../variables/organization';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
/**
* Validate that user with id [userId] can modify secrets with ids [secretIds]
* @param {Object} obj
* @param {Object} obj.userId - id of user to validate
* @param {Object} obj.secretIds - secret ids
* @returns {Secret[]} secrets
*/
const validateSecrets = async ({
userId,
secretIds
}: {
userId: string;
secretIds: string[];
}) => {
let secrets;
try {
secrets = await Secret.find({
_id: {
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
}
});
if (secrets.length != secretIds.length) {
throw BadRequestError({ message: 'Unable to validate some secrets' })
}
const userMemberships = await Membership.find({ user: userId })
const userMembershipById = _.keyBy(userMemberships, 'workspace');
const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString()));
// for each secret check if the secret belongs to a workspace the user is a member of
secrets.forEach((secret: ISecret) => {
if (workspaceIdsSet.has(secret.workspace.toString())) {
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: ABILITY_WRITE });
if (isDisallowed) {
throw UnauthorizedRequestError({ message: 'You do not have the required permissions to perform this action' });
}
} else {
throw BadRequestError({ message: 'You cannot edit secrets of a workspace you are not a member of' });
}
});
} catch (err) {
throw BadRequestError({ message: 'Unable to validate secrets' })
}
return secrets;
}
interface V1PushSecret {
ciphertextKey: string;
ivKey: string;
@@ -714,7 +663,6 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
};
export {
validateSecrets,
v1PushSecrets,
v2PushSecrets,
pullSecrets,
+102
View File
@@ -0,0 +1,102 @@
import { Types } from 'mongoose';
import {
User,
IUser,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
Secret,
ISecret
} from '../models';
import {
validateUserClientForSecrets
} from '../helpers/user';
import {
validateServiceTokenDataClientForSecrets
} from '../helpers/serviceTokenData';
import {
validateServiceAccountClientForSecrets
} from '../helpers/serviceAccount';
import { BadRequestError } from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
* Validate accepted clients for secrets with ids [secretIds]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {ServiceAccount} obj.serviceAccount - service account client
* @param {ServiceTokenData} obj.service - service token client
* @param {String[]} obj.secretIds - ids of secrets to validate against
*/
const validateClientForSecrets = async ({
authData,
secretIds,
requiredPermissions
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
secretIds: string[];
requiredPermissions: string[];
}) => {
let secrets: ISecret[] = [];
secrets = await Secret.find({
_id: {
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
}
});
if (secrets.length != secretIds.length) {
throw BadRequestError({ message: 'Failed to validate non-existent secrets' })
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
// TODO
await validateUserClientForSecrets({
user: authData.authPayload,
secrets,
requiredPermissions
});
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
// TODO
await validateServiceAccountClientForSecrets({
serviceAccount: authData.authPayload,
secrets,
requiredPermissions
});
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForSecrets({
serviceTokenData: authData.authPayload,
secrets,
requiredPermissions
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
// TODO
await validateUserClientForSecrets({
user: authData.authPayload,
secrets,
requiredPermissions
});
}
return secrets;
}
export {
validateClientForSecrets
}
+63
View File
@@ -0,0 +1,63 @@
import _ from 'lodash';
import { Types } from 'mongoose';
import {
IServiceAccount,
ISecret,
ServiceAccountWorkspacePermission
} from '../models';
/**
* Validate that serviceAccount (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {Object} obj.
*/
const validateServiceAccountClientForWorkspace = async ({
serviceAccount,
workspaceId,
environment,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
workspaceId: Types.ObjectId;
environment: string;
requiredPermissions: string[];
}) => {
// TODO
return [];
}
/**
* Validate that service account (client) can access secrets
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceAccount - service account client
* @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceAccountClientForSecrets = async ({
serviceAccount,
secrets,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
const permissions = await ServiceAccountWorkspacePermission.find({
serviceAccount: serviceAccount._id
});
const permissionsObj = _.keyBy(permissions, (p) => {
return `${p.workspace.toString()}-${p.environment}`
});
// TODO
return [];
}
export {
validateServiceAccountClientForWorkspace,
validateServiceAccountClientForSecrets
}
+99
View File
@@ -0,0 +1,99 @@
import { Types } from 'mongoose';
import {
ISecret,
IServiceTokenData
} from '../models';
import { UnauthorizedRequestError } from '../utils/errors';
/**
* Validate that service token (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceTokenDataClientForWorkspace = async ({
serviceTokenData,
workspaceId,
environment,
requiredPermissions
}: {
serviceTokenData: IServiceTokenData;
workspaceId: Types.ObjectId;
environment?: string;
requiredPermissions?: string[];
}) => {
if (!serviceTokenData.workspace.equals(workspaceId)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace'
});
}
if (serviceTokenData.environment !== environment) {
// case: invalid environment passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace environment'
});
}
requiredPermissions?.forEach((permission) => {
if (!serviceTokenData.permissions.includes(permission)) {
throw UnauthorizedRequestError({
message: `Failed service token authorization for the given workspace environment action: ${permission}`
});
}
});
}
/**
* Validate that service token (client) can access secrets
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceTokenDataClientForSecrets = async ({
serviceTokenData,
secrets,
requiredPermissions
}: {
serviceTokenData: IServiceTokenData;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
secrets.forEach((secret: ISecret) => {
if (!serviceTokenData.workspace.equals(secret.workspace)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace'
});
}
if (serviceTokenData.environment !== secret.environment) {
// case: invalid environment passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace environment'
});
}
requiredPermissions?.forEach((permission) => {
if (!serviceTokenData.permissions.includes(permission)) {
throw UnauthorizedRequestError({
message: `Failed service token authorization for the given workspace environment action: ${permission}`
});
}
});
});
}
export {
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataClientForSecrets
}
View File
+100 -2
View File
@@ -1,6 +1,18 @@
import * as Sentry from '@sentry/node';
import { IUser, User } from '../models';
import { Types } from 'mongoose';
import {
IUser,
ISecret,
User,
Membership
} from '../models';
import { sendMail } from './nodemailer';
import { validateMembership } from './membership';
import _ from 'lodash';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import {
ABILITY_WRITE
} from '../variables/organization';
/**
* Initialize a user under email [email]
@@ -146,4 +158,90 @@ const checkUserDevice = async ({
}
}
export { setupAccount, completeAccount, checkUserDevice };
/**
* Validate that user (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForWorkspace = async ({
user,
workspaceId,
environment,
requiredPermissions
}: {
user: IUser;
workspaceId: Types.ObjectId;
environment?: string;
requiredPermissions?: string[];
}) => {
// 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
return membership;
}
/**
* Validate that user (client) can access secrets with ids [secretIds]
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Secret[]} obj.secrets - secrets to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForSecrets = async ({
user,
secrets,
requiredPermissions
}: {
user: IUser;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
// TODO: consider refactor
const userMemberships = await Membership.find({ user: user._id })
const userMembershipById = _.keyBy(userMemberships, 'workspace');
const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString()));
// for each secret check if the secret belongs to a workspace the user is a member of
secrets.forEach((secret: ISecret) => {
if (workspaceIdsSet.has(secret.workspace.toString())) {
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: ABILITY_WRITE });
if (isDisallowed) {
throw UnauthorizedRequestError({
message: 'You do not have the required permissions to perform this action'
});
}
} else {
throw BadRequestError({
message: 'You cannot edit secrets of a workspace you are not a member of'
});
}
});
}
export {
setupAccount,
completeAccount,
checkUserDevice,
validateUserClientForWorkspace,
validateUserClientForSecrets
};
+71 -20
View File
@@ -5,47 +5,98 @@ import {
Bot,
Membership,
Key,
Secret
Secret,
User,
IUser,
ServiceAccountWorkspacePermission,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
} from '../models';
import { createBot } from '../helpers/bot';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData';
import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError } from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
* Validate accepted clients by id including [userId], [serviceAccountId],
* and [serviceTokenDataId] for workspace with id [workspaceId] based
* Validate accepted clients for workspace with id [workspaceId] based
* on any known permissions.
* @param {Object} obj
* @param {Types.ObjectId} obj.userId - id of user
* @param {User} obj.user - user client
* @param {ServiceAccount} obj.serviceAccount - service account client
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForWorkspace = async ({
userId,
serviceAccountId,
serviceTokenDataId,
authData,
workspaceId,
environment
environment,
requiredPermissions
}: {
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
workspaceId: Types.ObjectId;
environment?: string;
requiredPermissions?: string[];
}) => {
let membership;
if (userId) {
membership = await validateMembership({
userId,
workspaceId
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
// TODO: validate user against [requiredPermissions]
}
if (serviceAccountId) {
// TODO
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
});
if (!permission) throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given workspace environment'
});
// TODO: validate [requiredPermissions] against [permission]
}
if (serviceTokenDataId) {
// TODO
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
// TODO: validate [requiredPermissions] against [permission]
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
}
return ({