feat(rbac): added new zod validation and permission check for all org level api

This commit is contained in:
Akhil Mohan
2023-09-08 21:20:29 +05:30
parent 96437fd1b7
commit e342e88499
27 changed files with 1870 additions and 1593 deletions
+55 -39
View File
@@ -1,16 +1,10 @@
import { Types } from "mongoose";
import {
MembershipOrg,
} from "../models";
import {
validateMembershipOrg,
} from "../helpers/membershipOrg";
import {
MembershipOrgNotFoundError,
UnauthorizedRequestError,
} from "../utils/errors";
import { MembershipOrg } from "../models";
import { validateMembershipOrg } from "../helpers/membershipOrg";
import { MembershipOrgNotFoundError, UnauthorizedRequestError } from "../utils/errors";
import { AuthData } from "../interfaces/middleware";
import { ActorType } from "../ee/models";
import { z } from "zod";
/**
* Validate authenticated clients for organization membership with id [membershipOrgId] based
@@ -22,35 +16,57 @@ import { ActorType } from "../ee/models";
* @param {MembershipOrg} - validated organization membership
*/
export const validateClientForMembershipOrg = async ({
authData,
membershipOrgId,
acceptedRoles,
acceptedStatuses,
authData,
membershipOrgId,
acceptedRoles,
acceptedStatuses
}: {
authData: AuthData;
membershipOrgId: Types.ObjectId;
acceptedRoles: Array<"owner" | "admin" | "member">;
acceptedStatuses: Array<"invited" | "accepted">;
authData: AuthData;
membershipOrgId: Types.ObjectId;
acceptedRoles: Array<"owner" | "admin" | "member">;
acceptedStatuses: Array<"invited" | "accepted">;
}) => {
const membershipOrg = await MembershipOrg.findById(membershipOrgId);
const membershipOrg = await MembershipOrg.findById(membershipOrgId);
if (!membershipOrg) throw MembershipOrgNotFoundError({
message: "Failed to find organization membership ",
});
switch (authData.actor.type) {
case ActorType.USER:
await validateMembershipOrg({
userId: authData.authPayload._id,
organizationId: membershipOrg.organization,
acceptedRoles,
acceptedStatuses,
});
return membershipOrg;
case ActorType.SERVICE:
throw UnauthorizedRequestError({
message: "Failed service account client authorization for organization membership",
});
}
}
if (!membershipOrg)
throw MembershipOrgNotFoundError({
message: "Failed to find organization membership "
});
switch (authData.actor.type) {
case ActorType.USER:
await validateMembershipOrg({
userId: authData.authPayload._id,
organizationId: membershipOrg.organization,
acceptedRoles,
acceptedStatuses
});
return membershipOrg;
case ActorType.SERVICE:
throw UnauthorizedRequestError({
message: "Failed service account client authorization for organization membership"
});
}
};
export const DelOrgMembershipv1 = z.object({
params: z.object({
membershipOrgId: z.string().trim()
})
});
export const InviteUserToOrgv1 = z.object({
body: z.object({
inviteeEmail: z.string().trim().email(),
organizationId: z.string().trim()
})
});
export const VerifyUserToOrgv1 = z.object({
body: z.object({
email: z.string().trim().email(),
organizationId: z.string().trim(),
code: z.string().trim()
})
});
+159 -15
View File
@@ -1,12 +1,7 @@
import { Types } from "mongoose";
import {
IUser,
Organization,
} from "../models";
import {
OrganizationNotFoundError,
UnauthorizedRequestError,
} from "../utils/errors";
import { z } from "zod";
import { IUser, Organization } from "../models";
import { OrganizationNotFoundError, UnauthorizedRequestError } from "../utils/errors";
import { validateUserClientForOrganization } from "./user";
import { AuthData } from "../interfaces/middleware";
import { ActorType } from "../ee/models";
@@ -21,7 +16,7 @@ export const validateClientForOrganization = async ({
authData,
organizationId,
acceptedRoles,
acceptedStatuses,
acceptedStatuses
}: {
authData: AuthData;
organizationId: Types.ObjectId;
@@ -32,10 +27,10 @@ export const validateClientForOrganization = async ({
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization",
message: "Failed to find organization"
});
}
let membershipOrg;
switch (authData.actor.type) {
case ActorType.USER:
@@ -43,13 +38,162 @@ export const validateClientForOrganization = async ({
user: authData.authPayload as IUser,
organization,
acceptedRoles,
acceptedStatuses,
acceptedStatuses
});
return { organization, membershipOrg };
return { organization, membershipOrg };
case ActorType.SERVICE:
throw UnauthorizedRequestError({
message: "Failed service token authorization for organization",
message: "Failed service token authorization for organization"
});
}
};
};
export const GetOrgPlansTablev1 = z.object({
query: z.object({ billingCycle: z.enum(["monthly", "yearly"]) }),
params: z.object({ organizationId: z.string().trim() })
});
export const GetOrgPlanv1 = z.object({
params: z.object({ organizationId: z.string().trim() }),
query: z.object({ workspaceId: z.string().trim().optional() })
});
export const StartOrgTrailv1 = z.object({
params: z.object({ organizationId: z.string().trim() }),
body: z.object({ success_url: z.string().trim() })
});
export const GetOrgPlanBillingInfov1 = z.object({
params: z.object({ organizationId: z.string().trim() }),
query: z.object({ workspaceId: z.string().trim().optional() })
});
export const GetOrgPlanTablev1 = z.object({
params: z.object({ organizationId: z.string().trim() }),
query: z.object({ workspaceId: z.string().trim().optional() })
});
export const GetOrgBillingDetailsv1 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const UpdateOrgBillingDetailsv1 = z.object({
params: z.object({ organizationId: z.string().trim() }),
body: z.object({
email: z.string().trim().email().optional(),
name: z.string().trim().optional()
})
});
export const GetOrgPmtMethodsv1 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const CreateOrgPmtMethodv1 = z.object({
params: z.object({ organizationId: z.string().trim() }),
body: z.object({
success_url: z.string().trim(),
cancel_url: z.string().trim()
})
});
export const DelOrgPmtMethodv1 = z.object({
params: z.object({
organizationId: z.string().trim(),
pmtMethodId: z.string().trim()
})
});
export const GetOrgTaxIdsv1 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const CreateOrgTaxId = z.object({
params: z.object({ organizationId: z.string().trim() }),
body: z.object({
type: z.string().trim(),
value: z.string().trim()
})
});
export const DelOrgTaxIdv1 = z.object({
params: z.object({
organizationId: z.string().trim(),
taxId: z.string().trim()
})
});
export const GetOrgInvoicesv1 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const GetOrgLicencesv1 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const CreateOrgv1 = z.object({
body: z.object({
organizationName: z.string().trim()
})
});
export const GetOrgv1 = z.object({
params: z.object({
organizationId: z.string().trim()
})
});
export const GetOrgMembersv1 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const GetOrgWorkspacesv1 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const ChangeOrgNamev1 = z.object({
params: z.object({ organizationId: z.string().trim() }),
body: z.object({ name: z.string().trim() })
});
export const GetOrgIncidentContactv1 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const CreateOrgIncideContact = z.object({
params: z.object({ organizationId: z.string().trim() }),
body: z.object({ email: z.string().email().trim() })
});
export const DelOrgIncideContact = z.object({
params: z.object({ organizationId: z.string().trim() }),
body: z.object({ email: z.string().email().trim() })
});
export const CreateOrgPortalSessionv1 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const GetOrgMembersAndWsv1 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const GetOrgMembersv2 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const UpdateOrgMemberv2 = z.object({
params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }),
body: z.object({
role: z.string().trim()
})
});
export const DeleteOrgMemberv2 = z.object({
params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() })
});
export const GetOrgWorkspacesv2 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
+25
View File
@@ -0,0 +1,25 @@
import { z } from "zod";
export const CreateInstalLSessionv1 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const LinkInstallationToOrgv1 = z.object({
body: z.object({
installationId: z.number(),
sessionId: z.string().trim()
})
});
export const GetOrgInstallStatusv1 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const GetOrgRisksv1 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const UpdateRiskStatusv1 = z.object({
params: z.object({ organizationId: z.string().trim(), riskId: z.string().trim() }),
body: z.object({ status: z.string().trim() })
});
+28
View File
@@ -0,0 +1,28 @@
import { z } from "zod";
import { AuthProvider } from "../ee/models";
export const GetSsoConfigv1 = z.object({
query: z.object({ organizationId: z.string().trim() })
});
export const CreateSsoConfigv1 = z.object({
body: z.object({
organizationId: z.string().trim(),
authProvider: z.nativeEnum(AuthProvider),
isActive: z.boolean(),
entryPoint: z.string().trim(),
issuer: z.string().trim(),
cert: z.string().trim()
})
});
export const UpdateSsoConfigv1 = z.object({
body: z.object({
organizationId: z.string().trim(),
authProvider: z.nativeEnum(AuthProvider).optional(),
isActive: z.boolean().optional(),
entryPoint: z.string().trim().optional(),
issuer: z.string().trim().optional(),
cert: z.string().trim().optional()
})
});
+105 -112
View File
@@ -1,25 +1,14 @@
import net from "net";
import { Types } from "mongoose";
import {
IServiceTokenData,
IUser,
SecretBlindIndexData,
Workspace,
} from "../models";
import {
ActorType,
TrustedIP
} from "../ee/models";
import { IServiceTokenData, IUser, SecretBlindIndexData, Workspace } from "../models";
import { ActorType, TrustedIP } from "../ee/models";
import { validateUserClientForWorkspace } from "./user";
import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData";
import {
BadRequestError,
UnauthorizedRequestError,
WorkspaceNotFoundError,
} from "../utils/errors";
import { BadRequestError, UnauthorizedRequestError, WorkspaceNotFoundError } from "../utils/errors";
import { BotService } from "../services";
import { AuthData } from "../interfaces/middleware";
import { extractIPDetails } from "../utils/ip";
import { z } from "zod";
/**
* Validate authenticated clients for workspace with id [workspaceId] based
@@ -32,106 +21,110 @@ import { extractIPDetails } from "../utils/ip";
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
export const validateClientForWorkspace = async ({
authData,
workspaceId,
environment,
acceptedRoles,
requiredPermissions,
requireBlindIndicesEnabled,
requireE2EEOff,
checkIPAllowlist
authData,
workspaceId,
environment,
acceptedRoles,
requiredPermissions,
requireBlindIndicesEnabled,
requireE2EEOff,
checkIPAllowlist
}: {
authData: AuthData;
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<"admin" | "member">;
requiredPermissions?: string[];
requireBlindIndicesEnabled: boolean;
requireE2EEOff: boolean;
checkIPAllowlist: boolean;
authData: AuthData;
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<"admin" | "member">;
requiredPermissions?: string[];
requireBlindIndicesEnabled: boolean;
requireE2EEOff: boolean;
checkIPAllowlist: boolean;
}) => {
const workspace = await Workspace.findById(workspaceId);
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw WorkspaceNotFoundError({
message: "Failed to find workspace",
});
if (!workspace)
throw WorkspaceNotFoundError({
message: "Failed to find workspace"
});
if (requireBlindIndicesEnabled) {
// case: blind indices are not enabled for secrets in this workspace
// (i.e. workspace was created before blind indices were introduced
// and no admin has enabled it)
const secretBlindIndexData = await SecretBlindIndexData.exists({
workspace: new Types.ObjectId(workspaceId),
});
if (!secretBlindIndexData) throw UnauthorizedRequestError({
message: "Failed workspace authorization due to blind indices not being enabled",
});
}
if (requireE2EEOff) {
const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId);
if (isWorkspaceE2EE) throw BadRequestError({
message: "Failed workspace authorization due to end-to-end encryption not being disabled",
});
}
let membership;
switch (authData.actor.type) {
case ActorType.USER:
membership = await validateUserClientForWorkspace({
user: authData.authPayload as IUser,
workspaceId,
environment,
acceptedRoles,
requiredPermissions,
});
return ({ membership, workspace });
case ActorType.SERVICE:
if (checkIPAllowlist) {
const trustedIps = await TrustedIP.find({
workspace: workspaceId
});
if (trustedIps.length > 0) {
// case: check the IP address of the inbound request against trusted IPs
if (requireBlindIndicesEnabled) {
// case: blind indices are not enabled for secrets in this workspace
// (i.e. workspace was created before blind indices were introduced
// and no admin has enabled it)
const blockList = new net.BlockList();
for (const trustedIp of trustedIps) {
if (trustedIp.prefix !== undefined) {
blockList.addSubnet(
trustedIp.ipAddress,
trustedIp.prefix,
trustedIp.type
);
} else {
blockList.addAddress(
trustedIp.ipAddress,
trustedIp.type
);
}
}
const { type } = extractIPDetails(authData.ipAddress);
const check = blockList.check(authData.ipAddress, type);
if (!check) throw UnauthorizedRequestError({
message: "Failed workspace authorization"
});
}
}
const secretBlindIndexData = await SecretBlindIndexData.exists({
workspace: new Types.ObjectId(workspaceId)
});
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload as IServiceTokenData,
workspaceId,
environment,
requiredPermissions,
});
return {};
}
}
if (!secretBlindIndexData)
throw UnauthorizedRequestError({
message: "Failed workspace authorization due to blind indices not being enabled"
});
}
if (requireE2EEOff) {
const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId);
if (isWorkspaceE2EE)
throw BadRequestError({
message: "Failed workspace authorization due to end-to-end encryption not being disabled"
});
}
let membership;
switch (authData.actor.type) {
case ActorType.USER:
membership = await validateUserClientForWorkspace({
user: authData.authPayload as IUser,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
return { membership, workspace };
case ActorType.SERVICE:
if (checkIPAllowlist) {
const trustedIps = await TrustedIP.find({
workspace: workspaceId
});
if (trustedIps.length > 0) {
// case: check the IP address of the inbound request against trusted IPs
const blockList = new net.BlockList();
for (const trustedIp of trustedIps) {
if (trustedIp.prefix !== undefined) {
blockList.addSubnet(trustedIp.ipAddress, trustedIp.prefix, trustedIp.type);
} else {
blockList.addAddress(trustedIp.ipAddress, trustedIp.type);
}
}
const { type } = extractIPDetails(authData.ipAddress);
const check = blockList.check(authData.ipAddress, type);
if (!check)
throw UnauthorizedRequestError({
message: "Failed workspace authorization"
});
}
}
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload as IServiceTokenData,
workspaceId,
environment,
requiredPermissions
});
return {};
}
};
export const CreateWorkspacev1 = z.object({
body: z.object({
workspaceName: z.string().trim(),
organizationId: z.string().trim()
})
});