Checkpoint service accounts

This commit is contained in:
Tuan Dang
2023-03-18 13:34:06 +07:00
parent 273f4228d7
commit ebdcccb6ca
18 changed files with 404 additions and 64 deletions

View File

@@ -7,6 +7,7 @@ import * as serviceTokenDataController from './serviceTokenDataController';
import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController';
import * as secretsController from './secretsController';
import * as serviceAccountsController from './serviceAccountsController';
import * as environmentController from './environmentController';
import * as tagController from './tagController';
@@ -20,6 +21,7 @@ export {
apiKeyDataController,
secretController,
secretsController,
serviceAccountsController,
environmentController,
tagController
}

View File

@@ -1,9 +1,11 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
MembershipOrg,
Membership,
Workspace
Workspace,
ServiceAccount
} from '../../models';
import { deleteMembershipOrg } from '../../helpers/membershipOrg';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
@@ -260,37 +262,44 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
}
}
*/
let workspaces;
try {
const { organizationId } = req.params;
const { organizationId } = req.params;
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
'_id'
)
).map((w) => w._id.toString())
);
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
'_id'
)
).map((w) => w._id.toString())
);
workspaces = (
await Membership.find({
user: req.user._id
}).populate('workspace')
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization workspaces'
});
}
return res.status(200).send({
const workspaces = (
await Membership.find({
user: req.user._id
}).populate('workspace')
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
return res.status(200).send({
workspaces
});
}
/**
* Return service accounts for organization with id [organizationId]
* @param req
* @param res
*/
export const getOrganizationServiceAccounts = async (req: Request, res: Response) => {
const { organizationId } = req.params;
const serviceAccounts = await ServiceAccount.find({
organization: new Types.ObjectId(organizationId)
});
return res.status(200).send({
serviceAccounts
});
}

View File

@@ -0,0 +1,178 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import {
ServiceAccount,
ServiceAccountKey,
ServiceAccountPermission
} from '../../models';
import {
CreateServiceAccountDto
} from '../../interfaces/serviceAccounts/dto';
/**
* Create a new service account under organization with id [organizationId]
* that has access to workspaces [workspaces]
* @param req
* @param res
* @returns
*/
export const createServiceAccount = async (req: Request, res: Response) => {
const {
organizationId,
name,
publicKey,
expiresIn,
}: CreateServiceAccountDto = req.body;
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
const serviceAccount = await new ServiceAccount({
name,
organization: new Types.ObjectId(organizationId),
user: req.user,
publicKey,
expiresAt
}).save();
// await Promise.all(
// workspaces.map(async ({
// workspaceId,
// environments,
// permissions,
// encryptedKey,
// nonce
// }: {
// workspaceId: string;
// environments: string[];
// permissions: string[];
// encryptedKey: string;
// nonce: string;
// }) => {
// const serviceAccountKey = await new ServiceAccountKey({
// encryptedKey,
// nonce,
// sender: req.user._id,
// serviceAccount: serviceAccount._id,
// workspace: new Types.ObjectId(workspaceId)
// });
// console.log('serviceAccountKey: ', serviceAccountKey);
// await Promise.all(
// permissions.map(async (name: string) => {
// const permission = await new ServiceAccountPermission({
// serviceAccount: serviceAccount._id,
// name,
// workspace: new Types.ObjectId(workspaceId),
// environments
// }).save();
// console.log('permission: ', permission);
// })
// );
// })
// );
return res.status(200).send({
serviceAccount
});
}
// /**
// * Add a service account key to service account with id [serviceAccountId]
// * for workspace with id [workspaceId]
// * @param req
// * @param res
// * @returns
// */
// export const addServiceAccountKey = async (req: Request, res: Response) => {
// const {
// workspaceId,
// encryptedKey,
// nonce
// } = req.body;
// const serviceAccountKey = await new ServiceAccountKey({
// encryptedKey,
// nonce,
// sender: req.user._id,
// serviceAccount: req.serviceAccount._d,
// workspace: new Types.ObjectId(workspaceId)
// }).save();
// return serviceAccountKey;
// }
/**
* Delete service account with id [serviceAccountId]
* @param req
* @param res
* @returns
*/
export const deleteServiceAccount = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const serviceAccount = await ServiceAccount.findByIdAndDelete(serviceAccountId);
await ServiceAccountKey.deleteMany({
serviceAccount: new Types.ObjectId(serviceAccountId)
});
return res.status(200).send({
serviceAccount
});
}
export const addServiceAccountWorkspaceAccess = async (req: Request, res: Response) => {
const { serviceAccountId, workspaceId } = req.params;
const {
encryptedKey,
nonce,
permissions // should contain environments
} = req.body;
const serviceAccountKey = await new ServiceAccountKey({
encryptedKey,
nonce,
sender: req.user._id,
serviceAccount: req.serviceAccount._id,
workspace: new Types.ObjectId('workspaceId')
});
const serviceAccountPermissions = await Promise.all(
permissions.map
);
}
export const deleteServiceAccountWorkspaceAccess = async (req: Request, res: Response) => {
// TODO
}
// /**
// * Add a service account key to service account with id [serviceAccountId]
// * for workspace with id [workspaceId]
// * @param req
// * @param res
// * @returns
// */
// export const addServiceAccountKey = async (req: Request, res: Response) => {
// const {
// workspaceId,
// encryptedKey,
// nonce
// } = req.body;
// const serviceAccountKey = await new ServiceAccountKey({
// encryptedKey,
// nonce,
// sender: req.user._id,
// serviceAccount: req.serviceAccount._d,
// workspace: new Types.ObjectId(workspaceId)
// }).save();
// return serviceAccountKey;
// }

View File

@@ -56,6 +56,7 @@ import {
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
serviceAccounts as v2ServiceAccountsRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
@@ -148,6 +149,7 @@ const main = async () => {
app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new
app.use('/api/v2/api-key', v2APIKeyDataRouter);
// api docs

View File

@@ -0,0 +1,8 @@
interface CreateServiceAccountDto {
organizationId: string;
name: string;
publicKey: string;
expiresIn: number;
}
export default CreateServiceAccountDto;

View File

@@ -0,0 +1,5 @@
import CreateServiceAccountDto from './CreateServiceAccountDto';
export {
CreateServiceAccountDto
}

View File

@@ -10,6 +10,7 @@ import requireIntegrationAuth from './requireIntegrationAuth';
import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth';
import requireServiceTokenAuth from './requireServiceTokenAuth';
import requireServiceTokenDataAuth from './requireServiceTokenDataAuth';
import requireServiceAccountAuth from './requireServiceAccountAuth';
import requireSecretAuth from './requireSecretAuth';
import requireSecretsAuth from './requireSecretsAuth';
import validateRequest from './validateRequest';
@@ -27,6 +28,7 @@ export {
requireIntegrationAuthorizationAuth,
requireServiceTokenAuth,
requireServiceTokenDataAuth,
requireServiceAccountAuth,
requireSecretAuth,
requireSecretsAuth,
validateRequest

View File

@@ -2,6 +2,8 @@ import { Request, Response, NextFunction } from 'express';
import { IOrganization, MembershipOrg } from '../models';
import { UnauthorizedRequestError, ValidationError } from '../utils/errors';
type req = 'params' | 'body' | 'query';
/**
* Validate if user on request is a member with proper roles for organization
* on request params.
@@ -11,18 +13,22 @@ import { UnauthorizedRequestError, ValidationError } from '../utils/errors';
*/
const requireOrganizationAuth = ({
acceptedRoles,
acceptedStatuses
acceptedStatuses,
location = 'params'
}: {
acceptedRoles: string[];
acceptedStatuses: string[];
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// organization authorization middleware
const { organizationId } = req[location];
// validate organization membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: req.params.organizationId
organization: organizationId
}).populate<{ organization: IOrganization }>('organization');

View File

@@ -0,0 +1,39 @@
import { Request, Response, NextFunction } from 'express';
import { ServiceAccount } from '../models';
import {
AccountNotFoundError,
UnauthorizedRequestError
} from '../utils/errors';
type req = 'params' | 'body' | 'query';
const requireServiceAccountAuth = ({
acceptedRoles,
acceptedStatuses,
location = 'params'
}: {
acceptedRoles: string[];
acceptedStatuses: string[];
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const serviceAccountId = req[location].serviceAccountId;
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
// TODO: acceptedRoles and acceptedStatuses
if (!serviceAccount) {
return next(AccountNotFoundError({ message: 'Failed to locate Service Account' }));
}
if (serviceAccount.user.toString() !== req.user.id.toString()) {
return next(UnauthorizedRequestError({ message: 'Failed to authenticate the Service Account' }));
}
req.serviceAccount = serviceAccount;
next();
}
}
export default requireServiceAccountAuth;

View File

@@ -10,6 +10,9 @@ import MembershipOrg, { IMembershipOrg } from './membershipOrg';
import Organization, { IOrganization } from './organization';
import Secret, { ISecret } from './secret';
import ServiceToken, { IServiceToken } from './serviceToken';
import ServiceAccount, { IServiceAccount } from './serviceAccount'; // new
import ServiceAccountKey, { IServiceAccountKey } from './serviceAccountKey'; // new
import ServiceAccountPermission, { IServiceAccountPermission } from './serviceAccountPermission';
import TokenData, { ITokenData } from './tokenData';
import User, { IUser } from './user';
import UserAction, { IUserAction } from './userAction';
@@ -43,6 +46,12 @@ export {
ISecret,
ServiceToken,
IServiceToken,
ServiceAccount,
IServiceAccount,
ServiceAccountKey,
IServiceAccountKey,
ServiceAccountPermission,
IServiceAccountPermission,
TokenData,
ITokenData,
User,

View File

@@ -1,22 +0,0 @@
import { Schema, model, Types, Document } from 'mongoose';
export interface IPermission extends Document {
_id: Types.ObjectId;
name: string;
}
const permissionSchema = new Schema<IPermission>(
{
name: {
type: String,
required: true
}
},
{
timestamps: true
}
);
const Permission = model<IPermission>('Permission', permissionSchema);
export default Permission;

View File

@@ -3,9 +3,8 @@ import { Schema, model, Types, Document } from 'mongoose';
export interface IServiceAccount extends Document {
_id: Types.ObjectId;
name: string;
isActive: boolean;
organization: Types.ObjectId;
createdBy: Types.ObjectId;
user: Types.ObjectId;
publicKey: string;
expiresAt: Date;
}
@@ -16,16 +15,12 @@ const serviceAccountSchema = new Schema<IServiceAccount>(
type: String,
required: true
},
isActive: {
type: Boolean,
required: true
},
organization: {
type: Schema.Types.ObjectId,
ref: 'Organization',
required: true
},
createdBy: {
user: { // user who created the service account
type: Schema.Types.ObjectId,
ref: 'User',
required: true

View File

@@ -9,7 +9,7 @@ export interface IServiceAccountKey {
workspace: Types.ObjectId;
}
const serviceAccountSchema = new Schema<IServiceAccountKey>(
const serviceAccountKeySchema = new Schema<IServiceAccountKey>(
{
encryptedKey: {
type: String,
@@ -39,6 +39,6 @@ const serviceAccountSchema = new Schema<IServiceAccountKey>(
}
);
const ServiceAccountKey = model<IServiceAccountKey>('ServiceAccountKey', serviceAccountSchema);
const ServiceAccountKey = model<IServiceAccountKey>('ServiceAccountKey', serviceAccountKeySchema);
export default ServiceAccountKey;

View File

@@ -0,0 +1,37 @@
import { Schema, model, Types, Document } from 'mongoose';
export interface IServiceAccountPermission extends Document {
_id: Types.ObjectId;
serviceAccount: Types.ObjectId;
name: string;
workspace?: Types.ObjectId;
environment?: string;
}
const serviceAccountPermissionSchema = new Schema<IServiceAccountPermission>(
{
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount',
required: true
},
name: {
type: String,
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
},
environment: {
type: 'String'
}
},
{
timestamps: true
}
);
const ServiceAccountPermission = model<IServiceAccountPermission>('ServiceAccountPermission', serviceAccountPermissionSchema);
export default ServiceAccountPermission;

View File

@@ -6,6 +6,7 @@ import workspace from './workspace';
import secret from './secret'; // deprecated
import secrets from './secrets';
import serviceTokenData from './serviceTokenData';
import serviceAccounts from './serviceAccounts';
import apiKeyData from './apiKeyData';
import environment from "./environment"
import tags from "./tags"
@@ -19,6 +20,7 @@ export {
secret,
secrets,
serviceTokenData,
serviceAccounts,
apiKeyData,
environment,
tags

View File

@@ -6,7 +6,7 @@ import {
requireMembershipOrgAuth,
validateRequest
} from '../../middleware';
import { body, param, query } from 'express-validator';
import { body, param } from 'express-validator';
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables';
import { organizationsController } from '../../controllers/v2';
@@ -77,4 +77,18 @@ router.get(
organizationsController.getOrganizationWorkspaces
);
router.get(
'/:organizationId/service-accounts',
param('organizationId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
organizationsController.getOrganizationServiceAccounts
);
export default router;

View File

@@ -0,0 +1,53 @@
import express from 'express';
const router = express.Router();
import {
requireOrganizationAuth,
requireServiceAccountAuth
} from '../../middleware';
import { body } from 'express-validator';
import {
OWNER,
ADMIN,
MEMBER,
ACCEPTED
} from '../../variables';
import { serviceAccountsController } from '../../controllers/v2';
router.post(
'/',
body('organizationId').exists().isString().trim(),
body('name').exists().isString().trim(),
body('publicKey').exists().isString().trim(),
body('expiresIn'), // measured in ms
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
location: 'body'
}),
serviceAccountsController.createServiceAccount
);
// 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.delete(
'/:serviceAccountId/key/:serviceAccountKeyId',
requireServiceAccountAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
async (req, res) => {
// TODO: delete service account key id
}
);
export default router;

View File

@@ -19,6 +19,7 @@ declare global {
secrets: any;
secretSnapshot: any;
serviceToken: any;
serviceAccount: any;
accessToken: any;
serviceTokenData: any;
apiKeyData: any;