feat(rbac): added new validation to all routes and permission check to most

This commit is contained in:
Akhil Mohan
2023-09-08 21:22:36 +05:30
parent 520a553ea1
commit 549121f44e
78 changed files with 4518 additions and 3499 deletions
@@ -2,7 +2,7 @@ import { Request, Response } from "express";
import { Types } from "mongoose";
import { standardRequest } from "../../config/request";
import { getApps, getTeams, revokeAccess } from "../../integrations";
import { Bot, IntegrationAuth } from "../../models";
import { Bot, IntegrationAuth, Workspace } from "../../models";
import { EventType } from "../../ee/models";
import { IntegrationService } from "../../services";
import { EEAuditLogService } from "../../ee/services";
@@ -18,12 +18,25 @@ import {
getIntegrationOptions as getIntegrationOptionsFunc
} from "../../variables";
import { exchangeRefresh } from "../../integrations";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/integrationAuth";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { getIntegrationAuthAccessHelper } from "../../helpers";
import { ObjectId } from "mongodb";
/***
* Return integration authorization with id [integrationAuthId]
*/
export const getIntegrationAuth = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const {
params: { integrationAuthId }
} = await validateRequest(reqValidator.GetIntegrationAuthV1, req);
const integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth)
@@ -31,6 +44,15 @@ export const getIntegrationAuth = async (req: Request, res: Response) => {
message: "Failed to find integration authorization"
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
return res.status(200).send({
integrationAuth
});
@@ -51,15 +73,19 @@ export const getIntegrationOptions = async (req: Request, res: Response) => {
* @returns
*/
export const oAuthExchange = async (req: Request, res: Response) => {
const {
workspaceId,
code,
integration,
url
} = req.body;
const {
body: { integration, workspaceId, code, url }
} = await validateRequest(reqValidator.OauthExchangeV1, req);
if (!INTEGRATION_SET.has(integration)) throw new Error("Failed to validate integration");
const environments = req.membership.workspace?.environments || [];
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Integrations
);
const workspace = await Workspace.findById(workspaceId);
const environments = workspace?.environments || [];
if (environments.length === 0) {
throw new Error("Failed to get environments");
}
@@ -99,25 +125,16 @@ export const oAuthExchange = async (req: Request, res: Response) => {
export const saveIntegrationToken = async (req: Request, res: Response) => {
// TODO: refactor
// TODO: check if access token is valid for each integration
let integrationAuth;
const {
workspaceId,
accessId,
refreshToken,
accessToken,
url,
namespace,
integration
}: {
workspaceId: string;
accessId: string | undefined;
refreshToken: string | undefined;
accessToken: string | undefined;
url: string;
namespace: string;
integration: string;
} = req.body;
body: { workspaceId, integration, url, accessId, namespace, accessToken, refreshToken }
} = await validateRequest(reqValidator.SaveIntegrationAccessTokenV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Integrations
);
const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId),
@@ -169,7 +186,7 @@ export const saveIntegrationToken = async (req: Request, res: Response) => {
}
if (!integrationAuth) throw new Error("Failed to save integration access token");
await EEAuditLogService.createAuditLog(
req.authData,
{
@@ -195,13 +212,29 @@ export const saveIntegrationToken = async (req: Request, res: Response) => {
* @returns
*/
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
const teamId = req.query.teamId as string;
const workspaceSlug = req.query.workspaceSlug as string;
const {
params: { integrationAuthId },
query: { teamId, workspaceSlug }
} = await validateRequest(reqValidator.GetIntegrationAuthAppsV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken, accessId } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
const apps = await getApps({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
accessId: req.accessId,
integrationAuth: integrationAuth,
accessToken: accessToken,
accessId: accessId,
...(teamId && { teamId }),
...(workspaceSlug && { workspaceSlug })
});
@@ -218,9 +251,27 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
* @returns
*/
export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
const {
params: { integrationAuthId }
} = await validateRequest(reqValidator.GetIntegrationAuthTeamsV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
const teams = await getTeams({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
integrationAuth: integrationAuth,
accessToken: accessToken
});
return res.status(200).send({
@@ -235,7 +286,24 @@ export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
* @param res
*/
export const getIntegrationAuthVercelBranches = async (req: Request, res: Response) => {
const appId = req.query.appId as string;
const {
params: { integrationAuthId },
query: { appId }
} = await validateRequest(reqValidator.GetIntegrationAuthVercelBranchesV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface VercelBranch {
ref: string;
@@ -245,9 +313,9 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon
const params = new URLSearchParams({
projectId: appId,
...(req.integrationAuth.teamId
...(integrationAuth.teamId
? {
teamId: req.integrationAuth.teamId
teamId: integrationAuth.teamId
}
: {})
});
@@ -260,7 +328,7 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon
{
params,
headers: {
Authorization: `Bearer ${req.accessToken}`,
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
@@ -281,7 +349,24 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon
* @param res
*/
export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: Response) => {
const appId = req.query.appId as string;
const {
params: { integrationAuthId },
query: { appId }
} = await validateRequest(reqValidator.GetIntegrationAuthRailwayEnvironmentsV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface RailwayEnvironment {
node: {
@@ -331,7 +416,7 @@ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: R
},
{
headers: {
Authorization: `Bearer ${req.accessToken}`,
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
@@ -357,7 +442,24 @@ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: R
* @param res
*/
export const getIntegrationAuthRailwayServices = async (req: Request, res: Response) => {
const appId = req.query.appId as string;
const {
params: { integrationAuthId },
query: { appId }
} = await validateRequest(reqValidator.GetIntegrationAuthRailwayServicesV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface RailwayService {
node: {
@@ -422,7 +524,7 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo
},
{
headers: {
Authorization: `Bearer ${req.accessToken}`,
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
@@ -446,7 +548,6 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo
* @returns
*/
export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: Response) => {
interface WorkspaceResponse {
size: number;
page: number;
@@ -466,31 +567,46 @@ export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: R
updated_on: string;
}
const {
params: { integrationAuthId }
} = await validateRequest(reqValidator.GetIntegrationAuthBitbucketWorkspacesV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
const workspaces: Workspace[] = [];
let hasNextPage = true;
let workspaceUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/workspaces`
let workspaceUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/workspaces`;
while (hasNextPage) {
const { data }: { data: WorkspaceResponse } = await standardRequest.get(
workspaceUrl,
{
headers: {
Authorization: `Bearer ${req.accessToken}`,
"Accept-Encoding": "application/json"
}
const { data }: { data: WorkspaceResponse } = await standardRequest.get(workspaceUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
);
});
if (data?.values.length > 0) {
data.values.forEach((workspace) => {
workspaces.push(workspace)
})
workspaces.push(workspace);
});
}
if (data.next) {
workspaceUrl = data.next
workspaceUrl = data.next;
} else {
hasNextPage = false
hasNextPage = false;
}
}
@@ -501,13 +617,30 @@ export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: R
/**
* Return list of secret groups for Northflank project with id [appId]
* @param req
* @param res
* @returns
* @param req
* @param res
* @returns
*/
export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res: Response) => {
const appId = req.query.appId as string;
const {
params: { integrationAuthId },
query: { appId }
} = await validateRequest(reqValidator.GetIntegrationAuthNorthflankSecretGroupsV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface NorthflankSecretGroup {
id: string;
name: string;
@@ -515,43 +648,41 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res
priority: number;
projectId: string;
}
interface SecretGroup {
name: string;
groupId: string;
}
const secretGroups: SecretGroup[] = [];
if (appId && appId !== "") {
if (appId && appId !== "") {
let page = 1;
const perPage = 10;
let hasMorePages = true;
while(hasMorePages) {
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage),
filter: "all",
filter: "all"
});
const {
data: {
data: {
secrets
}
data: { secrets }
}
} = await standardRequest.get<{ data: { secrets: NorthflankSecretGroup[] }}>(
} = await standardRequest.get<{ data: { secrets: NorthflankSecretGroup[] } }>(
`${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${appId}/secrets`,
{
params,
headers: {
Authorization: `Bearer ${req.accessToken}`,
"Accept-Encoding": "application/json",
},
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
secrets.forEach((a: any) => {
secretGroups.push({
name: a.name,
@@ -566,11 +697,11 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res
page++;
}
}
return res.status(200).send({
secretGroups
});
}
};
/**
* Return list of build configs for TeamCity project with id [appId]
@@ -579,7 +710,23 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res
* @returns
*/
export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: Response) => {
const appId = req.query.appId as string;
const {
params: { integrationAuthId,appId },
} = await validateRequest(reqValidator.GetIntegrationAuthTeamCityBuildConfigsV1 , req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface TeamCityBuildConfig {
id: string;
@@ -630,30 +777,48 @@ export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res:
* @returns
*/
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
const integrationAuth = await revokeAccess({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
const {
params: { integrationAuthId }
} = await validateRequest(reqValidator.DeleteIntegrationAuthV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
if (!integrationAuth) return res.status(400).send({
message: "Failed to find integration authorization"
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Integrations
);
const deletedIntegrationAuth = await revokeAccess({
integrationAuth: integrationAuth,
accessToken: accessToken
});
if (!deletedIntegrationAuth)
return res.status(400).send({
message: "Failed to find integration authorization"
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UNAUTHORIZE_INTEGRATION,
metadata: {
integration: integrationAuth.integration
integration: deletedIntegrationAuth.integration
}
},
{
workspaceId: integrationAuth.workspace
workspaceId: deletedIntegrationAuth.workspace
}
);
return res.status(200).send({
integrationAuth
integrationAuth: deletedIntegrationAuth
});
};