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
@@ -8,14 +8,8 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
import { sendMail } from "../../helpers/nodemailer"; import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services"; import { TokenService } from "../../services";
import { EELicenseService } from "../../ee/services"; import { EELicenseService } from "../../ee/services";
import { import { ACCEPTED, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables";
ACCEPTED, import * as reqValidator from "../../validation/membershipOrg";
ADMIN,
INVITED,
MEMBER,
OWNER,
TOKEN_EMAIL_ORG_INVITATION
} from "../../variables";
import { import {
getJwtSignupLifetime, getJwtSignupLifetime,
getJwtSignupSecret, getJwtSignupSecret,
@@ -23,6 +17,13 @@ import {
getSmtpConfigured getSmtpConfigured
} from "../../config"; } from "../../config";
import { validateUserEmail } from "../../validation"; import { validateUserEmail } from "../../validation";
import { validateRequest } from "../../helpers/validation";
import {
GeneralPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../services/RoleService";
import { ForbiddenError } from "@casl/ability";
/** /**
* Delete organization membership with id [membershipOrgId] from organization * Delete organization membership with id [membershipOrgId] from organization
@@ -31,7 +32,9 @@ import { validateUserEmail } from "../../validation";
* @returns * @returns
*/ */
export const deleteMembershipOrg = async (req: Request, _res: Response) => { export const deleteMembershipOrg = async (req: Request, _res: Response) => {
const { membershipOrgId } = req.params; const {
params: { membershipOrgId }
} = await validateRequest(reqValidator.DelOrgMembershipv1, req);
// check if organization membership to delete exists // check if organization membership to delete exists
const membershipOrgToDelete = await MembershipOrg.findOne({ const membershipOrgToDelete = await MembershipOrg.findOne({
@@ -42,21 +45,14 @@ export const deleteMembershipOrg = async (req: Request, _res: Response) => {
throw new Error("Failed to delete organization membership that doesn't exist"); throw new Error("Failed to delete organization membership that doesn't exist");
} }
// check if user is a member and admin of the organization const { permission, membership: membershipOrg } = await getUserOrgPermissions(
// whose membership we wish to delete req.user._id,
const membershipOrg = await MembershipOrg.findOne({ membershipOrgToDelete.organization.toString()
user: req.user._id, );
organization: membershipOrgToDelete.organization ForbiddenError.from(permission).throwUnlessCan(
}); GeneralPermissionActions.Delete,
OrgPermissionSubjects.Member
if (!membershipOrg) { );
throw new Error("Failed to validate organization membership");
}
if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) {
// user is not an admin member of the organization
throw new Error("Insufficient role for deleting organization membership");
}
// delete organization membership // delete organization membership
await deleteMemberFromOrg({ await deleteMemberFromOrg({
@@ -96,20 +92,18 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
*/ */
export const inviteUserToOrganization = async (req: Request, res: Response) => { export const inviteUserToOrganization = async (req: Request, res: Response) => {
let inviteeMembershipOrg, completeInviteLink; let inviteeMembershipOrg, completeInviteLink;
const { organizationId, inviteeEmail } = req.body; const {
body: { inviteeEmail, organizationId }
} = await validateRequest(reqValidator.InviteUserToOrgv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Create,
OrgPermissionSubjects.Member
);
const host = req.headers.host; const host = req.headers.host;
const siteUrl = `${req.protocol}://${host}`; const siteUrl = `${req.protocol}://${host}`;
// validate membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: new Types.ObjectId(organizationId)
});
if (!membershipOrg) {
throw new Error("Failed to validate organization membership");
}
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
const ssoConfig = await SSOConfig.findOne({ const ssoConfig = await SSOConfig.findOne({
@@ -119,8 +113,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
if (ssoConfig && ssoConfig.isActive) { if (ssoConfig && ssoConfig.isActive) {
// case: SAML SSO is enabled for the organization // case: SAML SSO is enabled for the organization
return res.status(400).send({ return res.status(400).send({
message: message: "Failed to invite member due to SAML SSO configured for organization"
"Failed to invite member due to SAML SSO configured for organization"
}); });
} }
@@ -231,7 +224,10 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
*/ */
export const verifyUserToOrganization = async (req: Request, res: Response) => { export const verifyUserToOrganization = async (req: Request, res: Response) => {
let user; let user;
const { email, organizationId, code } = req.body;
const {
body: { organizationId, email, code }
} = await validateRequest(reqValidator.VerifyUserToOrgv1, req);
user = await User.findOne({ email }).select("+publicKey"); user = await User.findOne({ email }).select("+publicKey");
@@ -4,24 +4,34 @@ import {
Membership, Membership,
MembershipOrg, MembershipOrg,
Organization, Organization,
Workspace, Workspace
} from "../../models"; } from "../../models";
import { createOrganization as create } from "../../helpers/organization"; import { createOrganization as create } from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg"; import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { ACCEPTED, OWNER } from "../../variables"; import { ACCEPTED, OWNER } from "../../variables";
import { getLicenseServerUrl, getSiteURL } from "../../config"; import { getLicenseServerUrl, getSiteURL } from "../../config";
import { licenseServerKeyRequest } from "../../config/request"; import { licenseServerKeyRequest } from "../../config/request";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/organization";
import {
GeneralPermissionActions,
OrgPermissionSubjects,
WorkspacePermissionActions,
getUserOrgPermissions
} from "../../services/RoleService";
import { OrganizationNotFoundError } from "../../utils/errors";
import { ForbiddenError } from "@casl/ability";
export const getOrganizations = async (req: Request, res: Response) => { export const getOrganizations = async (req: Request, res: Response) => {
const organizations = ( const organizations = (
await MembershipOrg.find({ await MembershipOrg.find({
user: req.user._id, user: req.user._id,
status: ACCEPTED, status: ACCEPTED
}).populate("organization") }).populate("organization")
).map((m) => m.organization); ).map((m) => m.organization);
return res.status(200).send({ return res.status(200).send({
organizations, organizations
}); });
}; };
@@ -33,27 +43,25 @@ export const getOrganizations = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createOrganization = async (req: Request, res: Response) => { export const createOrganization = async (req: Request, res: Response) => {
const { organizationName } = req.body; const {
body: { organizationName }
if (organizationName.length < 1) { } = await validateRequest(reqValidator.CreateOrgv1, req);
throw new Error("Organization names must be at least 1-character long");
}
// create organization and add user as member // create organization and add user as member
const organization = await create({ const organization = await create({
email: req.user.email, email: req.user.email,
name: organizationName, name: organizationName
}); });
await addMembershipsOrg({ await addMembershipsOrg({
userIds: [req.user._id.toString()], userIds: [req.user._id.toString()],
organizationId: organization._id.toString(), organizationId: organization._id.toString(),
roles: [OWNER], roles: [OWNER],
statuses: [ACCEPTED], statuses: [ACCEPTED]
}); });
return res.status(200).send({ return res.status(200).send({
organization, organization
}); });
}; };
@@ -64,9 +72,22 @@ export const createOrganization = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getOrganization = async (req: Request, res: Response) => { export const getOrganization = async (req: Request, res: Response) => {
const organization = req.organization const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgv1, req);
// ensure user has membership
await getUserOrgPermissions(req.user._id, organizationId);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
return res.status(200).send({ return res.status(200).send({
organization, organization
}); });
}; };
@@ -77,14 +98,22 @@ export const getOrganization = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getOrganizationMembers = async (req: Request, res: Response) => { export const getOrganizationMembers = async (req: Request, res: Response) => {
const { organizationId } = req.params; const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Member
);
const users = await MembershipOrg.find({ const users = await MembershipOrg.find({
organization: organizationId, organization: organizationId
}).populate("user", "+publicKey"); }).populate("user", "+publicKey");
return res.status(200).send({ return res.status(200).send({
users, users
}); });
}; };
@@ -94,17 +123,22 @@ export const getOrganizationMembers = async (req: Request, res: Response) => {
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationWorkspaces = async ( export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId }
) => { } = await validateRequest(reqValidator.GetOrgWorkspacesv1, req);
const { organizationId } = req.params;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
WorkspacePermissionActions.Read,
OrgPermissionSubjects.Workspace
);
const workspacesSet = new Set( const workspacesSet = new Set(
( (
await Workspace.find( await Workspace.find(
{ {
organization: organizationId, organization: organizationId
}, },
"_id" "_id"
) )
@@ -113,14 +147,14 @@ export const getOrganizationWorkspaces = async (
const workspaces = ( const workspaces = (
await Membership.find({ await Membership.find({
user: req.user._id, user: req.user._id
}).populate("workspace") }).populate("workspace")
) )
.filter((m) => workspacesSet.has(m.workspace._id.toString())) .filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace); .map((m) => m.workspace);
return res.status(200).send({ return res.status(200).send({
workspaces, workspaces
}); });
}; };
@@ -131,24 +165,32 @@ export const getOrganizationWorkspaces = async (
* @returns * @returns
*/ */
export const changeOrganizationName = async (req: Request, res: Response) => { export const changeOrganizationName = async (req: Request, res: Response) => {
const { organizationId } = req.params; const {
const { name } = req.body; params: { organizationId },
body: { name }
} = await validateRequest(reqValidator.ChangeOrgNamev1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Edit,
OrgPermissionSubjects.Settings
);
const organization = await Organization.findOneAndUpdate( const organization = await Organization.findOneAndUpdate(
{ {
_id: organizationId, _id: organizationId
}, },
{ {
name, name
}, },
{ {
new: true, new: true
} }
); );
return res.status(200).send({ return res.status(200).send({
message: "Successfully changed organization name", message: "Successfully changed organization name",
organization, organization
}); });
}; };
@@ -158,18 +200,23 @@ export const changeOrganizationName = async (req: Request, res: Response) => {
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationIncidentContacts = async ( export const getOrganizationIncidentContacts = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId }
) => { } = await validateRequest(reqValidator.GetOrgIncidentContactv1, req);
const { organizationId } = req.params;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactsOrg = await IncidentContactOrg.find({ const incidentContactsOrg = await IncidentContactOrg.find({
organization: organizationId, organization: organizationId
}); });
return res.status(200).send({ return res.status(200).send({
incidentContactsOrg, incidentContactsOrg
}); });
}; };
@@ -179,12 +226,17 @@ export const getOrganizationIncidentContacts = async (
* @param res * @param res
* @returns * @returns
*/ */
export const addOrganizationIncidentContact = async ( export const addOrganizationIncidentContact = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId },
) => { body: { email }
const { organizationId } = req.params; } = await validateRequest(reqValidator.CreateOrgIncideContact, req);
const { email } = req.body;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Create,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate( const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
{ email, organization: organizationId }, { email, organization: organizationId },
@@ -193,7 +245,7 @@ export const addOrganizationIncidentContact = async (
); );
return res.status(200).send({ return res.status(200).send({
incidentContactOrg, incidentContactOrg
}); });
}; };
@@ -203,21 +255,26 @@ export const addOrganizationIncidentContact = async (
* @param res * @param res
* @returns * @returns
*/ */
export const deleteOrganizationIncidentContact = async ( export const deleteOrganizationIncidentContact = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId },
) => { body: { email }
const { organizationId } = req.params; } = await validateRequest(reqValidator.DelOrgIncideContact, req);
const { email } = req.body;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Delete,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({ const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
email, email,
organization: organizationId, organization: organizationId
}); });
return res.status(200).send({ return res.status(200).send({
message: "Successfully deleted organization incident contact", message: "Successfully deleted organization incident contact",
incidentContactOrg, incidentContactOrg
}); });
}; };
@@ -228,19 +285,41 @@ export const deleteOrganizationIncidentContact = async (
* @param res * @param res
* @returns * @returns
*/ */
export const createOrganizationPortalSession = async ( export const createOrganizationPortalSession = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId }
) => { } = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
const { data: { pmtMethods } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Create,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { pmtMethods }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`
); );
if (pmtMethods.length < 1) { if (pmtMethods.length < 1) {
// case: organization has no payment method on file // case: organization has no payment method on file
// -> redirect to add payment method portal // -> redirect to add payment method portal
const { data: { url } } = await licenseServerKeyRequest.post( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`,
{ {
success_url: (await getSiteURL()) + "/dashboard", success_url: (await getSiteURL()) + "/dashboard",
cancel_url: (await getSiteURL()) + "/dashboard" cancel_url: (await getSiteURL()) + "/dashboard"
@@ -250,8 +329,12 @@ export const createOrganizationPortalSession = async (
} else { } else {
// case: organization has payment method on file // case: organization has payment method on file
// -> redirect to billing portal // -> redirect to billing portal
const { data: { url } } = await licenseServerKeyRequest.post( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/billing-portal`, data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/billing-portal`,
{ {
return_url: (await getSiteURL()) + "/dashboard" return_url: (await getSiteURL()) + "/dashboard"
} }
@@ -266,29 +349,36 @@ export const createOrganizationPortalSession = async (
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationMembersAndTheirWorkspaces = async ( export const getOrganizationMembersAndTheirWorkspaces = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId }
) => { } = await validateRequest(reqValidator.GetOrgMembersv1, req);
const { organizationId } = req.params;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Member
);
ForbiddenError.from(permission).throwUnlessCan(
WorkspacePermissionActions.Read,
OrgPermissionSubjects.Workspace
);
const workspacesSet = ( const workspacesSet = (
await Workspace.find( await Workspace.find(
{ {
organization: organizationId, organization: organizationId
}, },
"_id" "_id"
) )
).map((w) => w._id.toString()); ).map((w) => w._id.toString());
const memberships = ( const memberships = await Membership.find({
await Membership.find({ workspace: { $in: workspacesSet }
workspace: { $in: workspacesSet }, }).populate("workspace");
}).populate("workspace")
);
const userToWorkspaceIds: any = {}; const userToWorkspaceIds: any = {};
memberships.forEach(membership => { memberships.forEach((membership) => {
const user = membership.user.toString(); const user = membership.user.toString();
if (userToWorkspaceIds[user]) { if (userToWorkspaceIds[user]) {
userToWorkspaceIds[user].push(membership.workspace); userToWorkspaceIds[user].push(membership.workspace);
+8 -8
View File
@@ -21,8 +21,8 @@ export const createRole = async (req: Request, res: Response) => {
body: { workspaceId, name, description, slug, permissions, orgId } body: { workspaceId, name, description, slug, permissions, orgId }
} = await validateRequest(CreateRoleSchema, req); } = await validateRequest(CreateRoleSchema, req);
const orgPermission = await getUserOrgPermissions(req.user.id, orgId); const { permission } = await getUserOrgPermissions(req.user.id, orgId);
if (orgPermission.cannot(GeneralPermissionActions.Create, OrgPermissionSubjects.Role)) { if (permission.cannot(GeneralPermissionActions.Create, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "User doesn't have the permission." }); throw BadRequestError({ message: "User doesn't have the permission." });
} }
@@ -59,8 +59,8 @@ export const updateRole = async (req: Request, res: Response) => {
} = await validateRequest(UpdateRoleSchema, req); } = await validateRequest(UpdateRoleSchema, req);
const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule
const orgPermission = await getUserOrgPermissions(req.user.id, orgId); const { permission } = await getUserOrgPermissions(req.user.id, orgId);
if (orgPermission.cannot(GeneralPermissionActions.Edit, OrgPermissionSubjects.Role)) { if (permission.cannot(GeneralPermissionActions.Edit, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "User doesn't have the permission." }); throw BadRequestError({ message: "User doesn't have the permission." });
} }
@@ -103,8 +103,8 @@ export const deleteRole = async (req: Request, res: Response) => {
throw BadRequestError({ message: "Role not found" }); throw BadRequestError({ message: "Role not found" });
} }
const orgPermission = await getUserOrgPermissions(req.user.id, role.organization.toString()); const { permission } = await getUserOrgPermissions(req.user.id, role.organization.toString());
if (orgPermission.cannot(GeneralPermissionActions.Delete, OrgPermissionSubjects.Role)) { if (permission.cannot(GeneralPermissionActions.Delete, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "User doesn't have the permission." }); throw BadRequestError({ message: "User doesn't have the permission." });
} }
await Role.findByIdAndDelete(role.id); await Role.findByIdAndDelete(role.id);
@@ -123,8 +123,8 @@ export const getRoles = async (req: Request, res: Response) => {
} = await validateRequest(GetRoleSchema, req); } = await validateRequest(GetRoleSchema, req);
const isOrgRole = !workspaceId; const isOrgRole = !workspaceId;
const orgPermission = await getUserOrgPermissions(req.user.id, orgId); const { permission } = await getUserOrgPermissions(req.user.id, orgId);
if (orgPermission.cannot(GeneralPermissionActions.Read, OrgPermissionSubjects.Role)) { if (permission.cannot(GeneralPermissionActions.Read, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "User doesn't have the permission." }); throw BadRequestError({ message: "User doesn't have the permission." });
} }
@@ -2,20 +2,45 @@ import { Request, Response } from "express";
import GitAppInstallationSession from "../../ee/models/gitAppInstallationSession"; import GitAppInstallationSession from "../../ee/models/gitAppInstallationSession";
import crypto from "crypto"; import crypto from "crypto";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { UnauthorizedRequestError } from "../../utils/errors"; import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
import GitAppOrganizationInstallation from "../../ee/models/gitAppOrganizationInstallation"; import GitAppOrganizationInstallation from "../../ee/models/gitAppOrganizationInstallation";
import { MembershipOrg } from "../../models";
import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository" import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository"
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE, STATUS_RESOLVED_NOT_REVOKED, STATUS_RESOLVED_REVOKED } from "../../ee/models/gitRisks"; import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE, STATUS_RESOLVED_NOT_REVOKED, STATUS_RESOLVED_REVOKED } from "../../ee/models/gitRisks";
import { ProbotOctokit } from "probot"; import { ProbotOctokit } from "probot";
import { Organization } from "../../models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secretScanning";
import {
GeneralPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../services/RoleService";
import { ForbiddenError } from "@casl/ability";
export const createInstallationSession = async (req: Request, res: Response) => { export const createInstallationSession = async (req: Request, res: Response) => {
const sessionId = crypto.randomBytes(16).toString("hex"); const sessionId = crypto.randomBytes(16).toString("hex");
const {
params: { organizationId }
} = await validateRequest(reqValidator.CreateInstalLSessionv1, req);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Create,
OrgPermissionSubjects.SecretScanning
);
await GitAppInstallationSession.findByIdAndUpdate( await GitAppInstallationSession.findByIdAndUpdate(
req.organization, organization,
{ {
organization: new Types.ObjectId(req.organization), organization: organization.id,
sessionId: sessionId, sessionId: sessionId,
user: new Types.ObjectId(req.user._id) user: new Types.ObjectId(req.user._id)
}, },
@@ -24,31 +49,43 @@ export const createInstallationSession = async (req: Request, res: Response) =>
res.send({ res.send({
sessionId: sessionId sessionId: sessionId
}) });
} };
export const linkInstallationToOrganization = async (req: Request, res: Response) => { export const linkInstallationToOrganization = async (req: Request, res: Response) => {
const { installationId, sessionId } = req.body const {
body: { sessionId, installationId }
} = await validateRequest(reqValidator.LinkInstallationToOrgv1, req);
const installationSession = await GitAppInstallationSession.findOneAndDelete({ sessionId: sessionId }) const installationSession = await GitAppInstallationSession.findOneAndDelete({
sessionId: sessionId
});
if (!installationSession) { if (!installationSession) {
throw UnauthorizedRequestError() throw UnauthorizedRequestError();
} }
const userMembership = await MembershipOrg.find({ user: req.user._id, organization: installationSession.organization }) const { permission } = await getUserOrgPermissions(
if (!userMembership) { req.user._id,
throw UnauthorizedRequestError() installationSession.organization.toString()
} );
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Edit,
OrgPermissionSubjects.SecretScanning
);
const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate({ const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate(
organizationId: installationSession.organization, {
}, { organizationId: installationSession.organization
},
{
installationId: installationId, installationId: installationId,
organizationId: installationSession.organization, organizationId: installationSession.organization,
user: installationSession.user user: installationSession.user
}, { },
{
upsert: true upsert: true
}).lean() }
).lean();
const octokit = new ProbotOctokit({ const octokit = new ProbotOctokit({
auth: { auth: {
@@ -66,41 +103,68 @@ export const linkInstallationToOrganization = async (req: Request, res: Response
} }
export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => { export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => {
const { organizationId } = req.params const { organizationId } = req.params;
try { try {
const appInstallation = await GitAppOrganizationInstallation.findOne({ organizationId: organizationId }).lean() const appInstallation = await GitAppOrganizationInstallation.findOne({
organizationId: organizationId
}).lean();
if (!appInstallation) { if (!appInstallation) {
res.json({ res.json({
appInstallationComplete: false appInstallationComplete: false
}) });
} }
res.json({ res.json({
appInstallationComplete: true appInstallationComplete: true
}) });
} catch { } catch {
res.json({ res.json({
appInstallationComplete: false appInstallationComplete: false
}) });
} }
} };
export const getRisksForOrganization = async (req: Request, res: Response) => { export const getRisksForOrganization = async (req: Request, res: Response) => {
const { organizationId } = req.params const {
const risks = await GitRisks.find({ organization: organizationId }).sort({ createdAt: -1 }).lean() params: { organizationId }
} = await validateRequest(reqValidator.GetOrgRisksv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.SecretScanning
);
const risks = await GitRisks.find({ organization: organizationId })
.sort({ createdAt: -1 })
.lean();
res.json({ res.json({
risks: risks risks: risks
}) });
} };
export const updateRisksStatus = async (req: Request, res: Response) => { export const updateRisksStatus = async (req: Request, res: Response) => {
const { riskId } = req.params const {
const { status } = req.body params: { organizationId, riskId },
const isRiskResolved = status == STATUS_RESOLVED_FALSE_POSITIVE || status == STATUS_RESOLVED_REVOKED || status == STATUS_RESOLVED_NOT_REVOKED ? true : false body: { status }
} = await validateRequest(reqValidator.UpdateRiskStatusv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Edit,
OrgPermissionSubjects.SecretScanning
);
const isRiskResolved =
status == STATUS_RESOLVED_FALSE_POSITIVE ||
status == STATUS_RESOLVED_REVOKED ||
status == STATUS_RESOLVED_NOT_REVOKED
? true
: false;
const risk = await GitRisks.findByIdAndUpdate(riskId, { const risk = await GitRisks.findByIdAndUpdate(riskId, {
status: status, status: status,
isResolved: isRiskResolved isResolved: isRiskResolved
}).lean() }).lean();
res.json(risk) res.json(risk);
} };
@@ -5,17 +5,21 @@ import {
Integration, Integration,
IntegrationAuth, IntegrationAuth,
Membership, Membership,
MembershipOrg, Organization,
ServiceToken, ServiceToken,
Workspace, Workspace
} from "../../models"; } from "../../models";
import { import { createWorkspace as create, deleteWorkspace as deleteWork } from "../../helpers/workspace";
createWorkspace as create,
deleteWorkspace as deleteWork,
} from "../../helpers/workspace";
import { EELicenseService } from "../../ee/services"; import { EELicenseService } from "../../ee/services";
import { addMemberships } from "../../helpers/membership"; import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables"; import { ADMIN } from "../../variables";
import { OrganizationNotFoundError } from "../../utils/errors";
import {
OrgPermissionSubjects,
WorkspacePermissionActions,
getUserOrgPermissions
} from "../../services/RoleService";
import { ForbiddenError } from "@casl/ability";
/** /**
* Return public keys of members of workspace with id [workspaceId] * Return public keys of members of workspace with id [workspaceId]
@@ -28,17 +32,17 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
const publicKeys = ( const publicKeys = (
await Membership.find({ await Membership.find({
workspace: workspaceId, workspace: workspaceId
}).populate<{ user: IUser }>("user", "publicKey") }).populate<{ user: IUser }>("user", "publicKey")
).map((member) => { ).map((member) => {
return { return {
publicKey: member.user.publicKey, publicKey: member.user.publicKey,
userId: member.user._id, userId: member.user._id
}; };
}); });
return res.status(200).send({ return res.status(200).send({
publicKeys, publicKeys
}); });
}; };
@@ -52,11 +56,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const users = await Membership.find({ const users = await Membership.find({
workspace: workspaceId, workspace: workspaceId
}).populate("user", "+publicKey"); }).populate("user", "+publicKey");
return res.status(200).send({ return res.status(200).send({
users, users
}); });
}; };
@@ -69,12 +73,12 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
export const getWorkspaces = async (req: Request, res: Response) => { export const getWorkspaces = async (req: Request, res: Response) => {
const workspaces = ( const workspaces = (
await Membership.find({ await Membership.find({
user: req.user._id, user: req.user._id
}).populate("workspace") }).populate("workspace")
).map((m) => m.workspace); ).map((m) => m.workspace);
return res.status(200).send({ return res.status(200).send({
workspaces, workspaces
}); });
}; };
@@ -88,11 +92,11 @@ export const getWorkspace = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const workspace = await Workspace.findOne({ const workspace = await Workspace.findOne({
_id: workspaceId, _id: workspaceId
}); });
return res.status(200).send({ return res.status(200).send({
workspace, workspace
}); });
}; };
@@ -106,16 +110,19 @@ export const getWorkspace = async (req: Request, res: Response) => {
export const createWorkspace = async (req: Request, res: Response) => { export const createWorkspace = async (req: Request, res: Response) => {
const { workspaceName, organizationId } = req.body; const { workspaceName, organizationId } = req.body;
// validate organization membership const organization = await Organization.findById(organizationId);
const membershipOrg = await MembershipOrg.findOne({ if (!organization) {
user: req.user._id, throw OrganizationNotFoundError({
organization: new Types.ObjectId(organizationId), message: "Failed to find organization"
}); });
if (!membershipOrg) {
throw new Error("Failed to validate organization membership");
} }
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
WorkspacePermissionActions.Create,
OrgPermissionSubjects.Workspace
);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (plan.workspaceLimit !== null) { if (plan.workspaceLimit !== null) {
@@ -123,7 +130,8 @@ export const createWorkspace = async (req: Request, res: Response) => {
if (plan.workspacesUsed >= plan.workspaceLimit) { if (plan.workspacesUsed >= plan.workspaceLimit) {
// case: number of workspaces used exceeds the number of workspaces allowed // case: number of workspaces used exceeds the number of workspaces allowed
return res.status(400).send({ return res.status(400).send({
message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces.", message:
"Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
}); });
} }
} }
@@ -135,17 +143,17 @@ export const createWorkspace = async (req: Request, res: Response) => {
// create workspace and add user as member // create workspace and add user as member
const workspace = await create({ const workspace = await create({
name: workspaceName, name: workspaceName,
organizationId: new Types.ObjectId(organizationId), organizationId: new Types.ObjectId(organizationId)
}); });
await addMemberships({ await addMemberships({
userIds: [req.user._id], userIds: [req.user._id],
workspaceId: workspace._id.toString(), workspaceId: workspace._id.toString(),
roles: [ADMIN], roles: [ADMIN]
}); });
return res.status(200).send({ return res.status(200).send({
workspace, workspace
}); });
}; };
@@ -160,11 +168,11 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
// delete workspace // delete workspace
await deleteWork({ await deleteWork({
id: workspaceId, id: workspaceId
}); });
return res.status(200).send({ return res.status(200).send({
message: "Successfully deleted workspace", message: "Successfully deleted workspace"
}); });
}; };
@@ -180,19 +188,19 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
const workspace = await Workspace.findOneAndUpdate( const workspace = await Workspace.findOneAndUpdate(
{ {
_id: workspaceId, _id: workspaceId
}, },
{ {
name, name
}, },
{ {
new: true, new: true
} }
); );
return res.status(200).send({ return res.status(200).send({
message: "Successfully changed workspace name", message: "Successfully changed workspace name",
workspace, workspace
}); });
}; };
@@ -206,11 +214,11 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const integrations = await Integration.find({ const integrations = await Integration.find({
workspace: workspaceId, workspace: workspaceId
}); });
return res.status(200).send({ return res.status(200).send({
integrations, integrations
}); });
}; };
@@ -220,18 +228,15 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceIntegrationAuthorizations = async ( export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: Response) => {
req: Request,
res: Response
) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const authorizations = await IntegrationAuth.find({ const authorizations = await IntegrationAuth.find({
workspace: workspaceId, workspace: workspaceId
}); });
return res.status(200).send({ return res.status(200).send({
authorizations, authorizations
}); });
}; };
@@ -241,18 +246,15 @@ export const getWorkspaceIntegrationAuthorizations = async (
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceServiceTokens = async ( export const getWorkspaceServiceTokens = async (req: Request, res: Response) => {
req: Request,
res: Response
) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
// ?? FIX. // ?? FIX.
const serviceTokens = await ServiceToken.find({ const serviceTokens = await ServiceToken.find({
user: req.user._id, user: req.user._id,
workspace: workspaceId, workspace: workspaceId
}); });
return res.status(200).send({ return res.status(200).send({
serviceTokens, serviceTokens
}); });
}; };
@@ -6,6 +6,15 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
import Role from "../../models/role"; import Role from "../../models/role";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
import { CUSTOM } from "../../variables"; import { CUSTOM } from "../../variables";
import * as reqValidator from "../../validation/organization";
import { validateRequest } from "../../helpers/validation";
import {
GeneralPermissionActions,
OrgPermissionSubjects,
WorkspacePermissionActions,
getUserOrgPermissions
} from "../../services/RoleService";
import { ForbiddenError } from "@casl/ability";
/** /**
* Return memberships for organization with id [organizationId] * Return memberships for organization with id [organizationId]
@@ -46,7 +55,15 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
} }
} }
*/ */
const { organizationId } = req.params; const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv2, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Member
);
const memberships = await MembershipOrg.find({ const memberships = await MembershipOrg.find({
organization: organizationId organization: organizationId
@@ -116,8 +133,15 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
} }
} }
*/ */
const { membershipId } = req.params; const {
const { role } = req.body; params: { organizationId, membershipId },
body: { role }
} = await validateRequest(reqValidator.UpdateOrgMemberv2, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Edit,
OrgPermissionSubjects.Member
);
const isCustomRole = !["admin", "member", "owner"].includes(role); const isCustomRole = !["admin", "member", "owner"].includes(role);
if (isCustomRole) { if (isCustomRole) {
@@ -191,7 +215,14 @@ export const deleteOrganizationMembership = async (req: Request, res: Response)
} }
} }
*/ */
const { membershipId } = req.params; const {
params: { organizationId, membershipId }
} = await validateRequest(reqValidator.DeleteOrgMemberv2, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Delete,
OrgPermissionSubjects.Member
);
// delete organization membership // delete organization membership
const membership = await deleteMembershipOrg({ const membership = await deleteMembershipOrg({
@@ -247,7 +278,15 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
} }
} }
*/ */
const { organizationId } = req.params; const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgWorkspacesv2, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
WorkspacePermissionActions.Read,
OrgPermissionSubjects.Workspace
);
const workspacesSet = new Set( const workspacesSet = new Set(
( (
@@ -3,30 +3,60 @@ import { Request, Response } from "express";
import { getLicenseServerUrl } from "../../../config"; import { getLicenseServerUrl } from "../../../config";
import { licenseServerKeyRequest } from "../../../config/request"; import { licenseServerKeyRequest } from "../../../config/request";
import { EELicenseService } from "../../services"; import { EELicenseService } from "../../services";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/organization";
import {
GeneralPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../../services/RoleService";
import { ForbiddenError } from "@casl/ability";
import { Organization } from "../../../models";
import { OrganizationNotFoundError } from "../../../utils/errors";
export const getOrganizationPlansTable = async (req: Request, res: Response) => { export const getOrganizationPlansTable = async (req: Request, res: Response) => {
const billingCycle = req.query.billingCycle as string; const {
query: { billingCycle },
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlansTablev1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const { data } = await licenseServerKeyRequest.get( const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
); );
return res.status(200).send(data); return res.status(200).send(data);
} };
/** /**
* Return the organization current plan's feature set * Return the organization current plan's feature set
*/ */
export const getOrganizationPlan = async (req: Request, res: Response) => { export const getOrganizationPlan = async (req: Request, res: Response) => {
const { organizationId } = req.params; const {
const workspaceId = req.query.workspaceId as string; query: { workspaceId },
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanv1, req);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId), new Types.ObjectId(workspaceId)); const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const plan = await EELicenseService.getPlan(
new Types.ObjectId(organizationId),
new Types.ObjectId(workspaceId)
);
return res.status(200).send({ return res.status(200).send({
plan, plan
}); });
} };
/** /**
* Return checkout url for pro trial * Return checkout url for pro trial
@@ -35,11 +65,34 @@ export const getOrganizationPlan = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const startOrganizationTrial = async (req: Request, res: Response) => { export const startOrganizationTrial = async (req: Request, res: Response) => {
const { organizationId } = req.params; const {
const { success_url } = req.body; params: { organizationId },
body: { success_url }
} = await validateRequest(reqValidator.StartOrgTrailv1, req);
const { data: { url } } = await licenseServerKeyRequest.post( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/session/trial`, ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Create,
OrgPermissionSubjects.Billing
);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Edit,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/session/trial`,
{ {
success_url success_url
} }
@@ -50,7 +103,7 @@ export const startOrganizationTrial = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
url url
}); });
} };
/** /**
* Return the organization's current plan's billing info * Return the organization's current plan's billing info
@@ -59,12 +112,31 @@ export const startOrganizationTrial = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => { export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.get( const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/billing` `${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/cloud-plan/billing`
); );
return res.status(200).send(data); return res.status(200).send(data);
} };
/** /**
* Return the organization's current plan's feature table * Return the organization's current plan's feature table
@@ -73,29 +145,82 @@ export const getOrganizationPlanBillingInfo = async (req: Request, res: Response
* @returns * @returns
*/ */
export const getOrganizationPlanTable = async (req: Request, res: Response) => { export const getOrganizationPlanTable = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanTablev1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.get( const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/table` `${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/cloud-plan/table`
); );
return res.status(200).send(data); return res.status(200).send(data);
} };
export const getOrganizationBillingDetails = async (req: Request, res: Response) => { export const getOrganizationBillingDetails = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgBillingDetailsv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.get( const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details` `${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details`
); );
return res.status(200).send(data); return res.status(200).send(data);
} };
export const updateOrganizationBillingDetails = async (req: Request, res: Response) => { export const updateOrganizationBillingDetails = async (req: Request, res: Response) => {
const { const {
name, params: { organizationId },
email body: { name, email }
} = req.body; } = await validateRequest(reqValidator.UpdateOrgBillingDetailsv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Edit,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.patch( const { data } = await licenseServerKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details`, `${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details`,
{ {
...(name ? { name } : {}), ...(name ? { name } : {}),
...(email ? { email } : {}) ...(email ? { email } : {})
@@ -103,40 +228,78 @@ export const updateOrganizationBillingDetails = async (req: Request, res: Respon
); );
return res.status(200).send(data); return res.status(200).send(data);
} };
/** /**
* Return the organization's payment methods on file * Return the organization's payment methods on file
*/ */
export const getOrganizationPmtMethods = async (req: Request, res: Response) => { export const getOrganizationPmtMethods = async (req: Request, res: Response) => {
const { data: { pmtMethods } } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods` params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPmtMethodsv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { pmtMethods }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`
); );
return res.status(200).send(pmtMethods); return res.status(200).send(pmtMethods);
} };
/** /**
* Return URL to add payment method for organization * Return URL to add payment method for organization
*/ */
export const addOrganizationPmtMethod = async (req: Request, res: Response) => { export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
const { const {
success_url, params: { organizationId },
cancel_url, body: { success_url, cancel_url }
} = req.body; } = await validateRequest(reqValidator.CreateOrgPmtMethodv1, req);
const { data: { url } } = await licenseServerKeyRequest.post( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Create,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`,
{ {
success_url, success_url,
cancel_url, cancel_url
} }
); );
return res.status(200).send({ return res.status(200).send({
url, url
}); });
} };
/** /**
* Delete payment method with id [pmtMethodId] for organization * Delete payment method with id [pmtMethodId] for organization
@@ -145,37 +308,90 @@ export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => { export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => {
const { pmtMethodId } = req.params; const {
params: { organizationId, pmtMethodId }
} = await validateRequest(reqValidator.DelOrgPmtMethodv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Delete,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.delete( const { data } = await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods/${pmtMethodId}`, `${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods/${pmtMethodId}`
); );
return res.status(200).send(data); return res.status(200).send(data);
} };
/** /**
* Return the organization's tax ids on file * Return the organization's tax ids on file
*/ */
export const getOrganizationTaxIds = async (req: Request, res: Response) => { export const getOrganizationTaxIds = async (req: Request, res: Response) => {
const { data: { tax_ids } } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids` params: { organizationId }
} = await validateRequest(reqValidator.GetOrgTaxIdsv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { tax_ids }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/tax-ids`
); );
return res.status(200).send(tax_ids); return res.status(200).send(tax_ids);
} };
/** /**
* Add tax id to organization * Add tax id to organization
*/ */
export const addOrganizationTaxId = async (req: Request, res: Response) => { export const addOrganizationTaxId = async (req: Request, res: Response) => {
const { const {
type, params: { organizationId },
value body: { type, value }
} = req.body; } = await validateRequest(reqValidator.CreateOrgTaxId, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Create,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.post( const { data } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids`, `${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/tax-ids`,
{ {
type, type,
value value
@@ -183,7 +399,7 @@ export const addOrganizationTaxId = async (req: Request, res: Response) => {
); );
return res.status(200).send(data); return res.status(200).send(data);
} };
/** /**
* Delete tax id with id [taxId] from organization tax ids on file * Delete tax id with id [taxId] from organization tax ids on file
@@ -192,14 +408,31 @@ export const addOrganizationTaxId = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteOrganizationTaxId = async (req: Request, res: Response) => { export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
const { taxId } = req.params; const {
params: { organizationId, taxId }
} = await validateRequest(reqValidator.DelOrgTaxIdv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Delete,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.delete( const { data } = await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids/${taxId}`, `${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/tax-ids/${taxId}`
); );
return res.status(200).send(data); return res.status(200).send(data);
} };
/** /**
* Return organization's invoices on file * Return organization's invoices on file
@@ -208,12 +441,33 @@ export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getOrganizationInvoices = async (req: Request, res: Response) => { export const getOrganizationInvoices = async (req: Request, res: Response) => {
const { data: { invoices } } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices` params: { organizationId }
} = await validateRequest(reqValidator.GetOrgInvoicesv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { invoices }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/invoices`
); );
return res.status(200).send(invoices); return res.status(200).send(invoices);
} };
/** /**
* Return organization's licenses on file * Return organization's licenses on file
@@ -222,9 +476,30 @@ export const getOrganizationInvoices = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getOrganizationLicenses = async (req: Request, res: Response) => { export const getOrganizationLicenses = async (req: Request, res: Response) => {
const { data: { licenses } } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/licenses` params: { organizationId }
} = await validateRequest(reqValidator.GetOrgLicencesv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { licenses }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/licenses`
); );
return res.status(200).send(licenses); return res.status(200).send(licenses);
} };
+58 -39
View File
@@ -2,16 +2,20 @@ import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { BotOrgService } from "../../../services"; import { BotOrgService } from "../../../services";
import { SSOConfig } from "../../models"; import { SSOConfig } from "../../models";
import { import { AuthMethod, MembershipOrg, User } from "../../../models";
AuthMethod,
MembershipOrg,
User
} from "../../../models";
import { getSSOConfigHelper } from "../../helpers/organizations"; import { getSSOConfigHelper } from "../../helpers/organizations";
import { client } from "../../../config"; import { client } from "../../../config";
import { ResourceNotFoundError } from "../../../utils/errors"; import { ResourceNotFoundError } from "../../../utils/errors";
import { getSiteURL } from "../../../config"; import { getSiteURL } from "../../../config";
import { EELicenseService } from "../../services"; import { EELicenseService } from "../../services";
import * as reqValidator from "../../../validation/sso";
import { validateRequest } from "../../../helpers/validation";
import {
GeneralPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../../services/RoleService";
import { ForbiddenError } from "@casl/ability";
/** /**
* Redirect user to appropriate SSO endpoint after successful authentication * Redirect user to appropriate SSO endpoint after successful authentication
@@ -22,11 +26,15 @@ import { EELicenseService } from "../../services";
*/ */
export const redirectSSO = async (req: Request, res: Response) => { export const redirectSSO = async (req: Request, res: Response) => {
if (req.isUserCompleted) { if (req.isUserCompleted) {
return res.redirect(`${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`); return res.redirect(
`${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`
);
} }
return res.redirect(`${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`); return res.redirect(
} `${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`
);
};
/** /**
* Return organization SAML SSO configuration * Return organization SAML SSO configuration
@@ -35,14 +43,22 @@ export const redirectSSO = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getSSOConfig = async (req: Request, res: Response) => { export const getSSOConfig = async (req: Request, res: Response) => {
const organizationId = req.query.organizationId as string; const {
query: { organizationId }
} = await validateRequest(reqValidator.GetSsoConfigv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Sso
);
const data = await getSSOConfigHelper({ const data = await getSSOConfigHelper({
organizationId: new Types.ObjectId(organizationId) organizationId: new Types.ObjectId(organizationId)
}); });
return res.status(200).send(data); return res.status(200).send(data);
} };
/** /**
* Update organization SAML SSO configuration * Update organization SAML SSO configuration
@@ -52,18 +68,21 @@ export const getSSOConfig = async (req: Request, res: Response) => {
*/ */
export const updateSSOConfig = async (req: Request, res: Response) => { export const updateSSOConfig = async (req: Request, res: Response) => {
const { const {
organizationId, body: { organizationId, authProvider, isActive, entryPoint, issuer, cert }
authProvider, } = await validateRequest(reqValidator.UpdateSsoConfigv1, req);
isActive,
entryPoint, const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
issuer, ForbiddenError.from(permission).throwUnlessCan(
cert, GeneralPermissionActions.Edit,
} = req.body; OrgPermissionSubjects.Sso
);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (!plan.samlSSO) return res.status(400).send({ if (!plan.samlSSO)
message: "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." return res.status(400).send({
message:
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
}); });
interface PatchUpdate { interface PatchUpdate {
@@ -90,9 +109,7 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
update.isActive = isActive; update.isActive = isActive;
} }
const key = await BotOrgService.getSymmetricKey( const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
new Types.ObjectId(organizationId)
);
if (entryPoint) { if (entryPoint) {
const { const {
@@ -140,7 +157,8 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
} }
); );
if (!ssoConfig) throw ResourceNotFoundError({ if (!ssoConfig)
throw ResourceNotFoundError({
message: "Failed to find SSO config to update" message: "Failed to find SSO config to update"
}); });
@@ -157,7 +175,7 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
} }
}, },
{ {
authMethods: [ssoConfig.authProvider], authMethods: [ssoConfig.authProvider]
} }
); );
} else { } else {
@@ -168,14 +186,14 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
} }
}, },
{ {
authMethods: [AuthMethod.EMAIL], authMethods: [AuthMethod.EMAIL]
} }
); );
} }
} }
return res.status(200).send(ssoConfig); return res.status(200).send(ssoConfig);
} };
/** /**
* Create organization SAML SSO configuration * Create organization SAML SSO configuration
@@ -185,23 +203,24 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
*/ */
export const createSSOConfig = async (req: Request, res: Response) => { export const createSSOConfig = async (req: Request, res: Response) => {
const { const {
organizationId, body: { organizationId, authProvider, isActive, entryPoint, issuer, cert }
authProvider, } = await validateRequest(reqValidator.CreateSsoConfigv1, req);
isActive,
entryPoint, const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
issuer, ForbiddenError.from(permission).throwUnlessCan(
cert GeneralPermissionActions.Create,
} = req.body; OrgPermissionSubjects.Sso
);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (!plan.samlSSO) return res.status(400).send({ if (!plan.samlSSO)
message: "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration." return res.status(400).send({
message:
"Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration."
}); });
const key = await BotOrgService.getSymmetricKey( const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
new Types.ObjectId(organizationId)
);
const { const {
ciphertext: encryptedEntryPoint, ciphertext: encryptedEntryPoint,
@@ -237,4 +256,4 @@ export const createSSOConfig = async (req: Request, res: Response) => {
}).save(); }).save();
return res.status(200).send(ssoConfig); return res.status(200).send(ssoConfig);
} };
+17 -127
View File
@@ -1,236 +1,126 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { import { requireAuth } from "../../../middleware";
requireAuth,
requireOrganizationAuth,
validateRequest,
} from "../../../middleware";
import { body, param, query } from "express-validator";
import { organizationsController } from "../../controllers/v1"; import { organizationsController } from "../../controllers/v1";
import { import { AuthMode } from "../../../variables";
ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER
} from "../../../variables";
router.get( router.get(
"/:organizationId/plans/table", "/:organizationId/plans/table",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("billingCycle").exists().isString().isIn(["monthly", "yearly"]),
validateRequest,
organizationsController.getOrganizationPlansTable organizationsController.getOrganizationPlansTable
); );
router.get( router.get(
"/:organizationId/plan", "/:organizationId/plan",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("workspaceId").optional().isString(),
validateRequest,
organizationsController.getOrganizationPlan organizationsController.getOrganizationPlan
); );
router.post( router.post(
"/:organizationId/session/trial", "/:organizationId/session/trial",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("success_url").exists().trim(),
validateRequest,
organizationsController.startOrganizationTrial organizationsController.startOrganizationTrial
); );
router.get( router.get(
"/:organizationId/plan/billing", "/:organizationId/plan/billing",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("workspaceId").optional().isString(),
validateRequest,
organizationsController.getOrganizationPlanBillingInfo organizationsController.getOrganizationPlanBillingInfo
); );
router.get( router.get(
"/:organizationId/plan/table", "/:organizationId/plan/table",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("workspaceId").optional().isString(),
validateRequest,
organizationsController.getOrganizationPlanTable organizationsController.getOrganizationPlanTable
); );
router.get( router.get(
"/:organizationId/billing-details", "/:organizationId/billing-details",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationBillingDetails organizationsController.getOrganizationBillingDetails
); );
router.patch( router.patch(
"/:organizationId/billing-details", "/:organizationId/billing-details",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("email").optional().isString().trim(),
body("name").optional().isString().trim(),
validateRequest,
organizationsController.updateOrganizationBillingDetails organizationsController.updateOrganizationBillingDetails
); );
router.get( router.get(
"/:organizationId/billing-details/payment-methods", "/:organizationId/billing-details/payment-methods",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationPmtMethods organizationsController.getOrganizationPmtMethods
); );
router.post( router.post(
"/:organizationId/billing-details/payment-methods", "/:organizationId/billing-details/payment-methods",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("success_url").exists().isString(),
body("cancel_url").exists().isString(),
validateRequest,
organizationsController.addOrganizationPmtMethod organizationsController.addOrganizationPmtMethod
); );
router.delete( router.delete(
"/:organizationId/billing-details/payment-methods/:pmtMethodId", "/:organizationId/billing-details/payment-methods/:pmtMethodId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
param("pmtMethodId").exists().trim(),
validateRequest,
organizationsController.deleteOrganizationPmtMethod organizationsController.deleteOrganizationPmtMethod
); );
router.get( router.get(
"/:organizationId/billing-details/tax-ids", "/:organizationId/billing-details/tax-ids",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationTaxIds organizationsController.getOrganizationTaxIds
); );
router.post( router.post(
"/:organizationId/billing-details/tax-ids", "/:organizationId/billing-details/tax-ids",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("type").exists().isString(),
body("value").exists().isString(),
validateRequest,
organizationsController.addOrganizationTaxId organizationsController.addOrganizationTaxId
); );
router.delete( router.delete(
"/:organizationId/billing-details/tax-ids/:taxId", "/:organizationId/billing-details/tax-ids/:taxId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
param("taxId").exists().trim(),
validateRequest,
organizationsController.deleteOrganizationTaxId organizationsController.deleteOrganizationTaxId
); );
router.get( router.get(
"/:organizationId/invoices", "/:organizationId/invoices",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationInvoices organizationsController.getOrganizationInvoices
); );
router.get( router.get(
"/:organizationId/licenses", "/:organizationId/licenses",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationLicenses organizationsController.getOrganizationLicenses
); );
+13 -41
View File
@@ -1,80 +1,52 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { requireAuth } from "../../../middleware";
import { import {
requireAuth, createInstallationSession,
requireOrganizationAuth, getCurrentOrganizationInstallationStatus,
validateRequest, getRisksForOrganization,
} from "../../../middleware"; linkInstallationToOrganization,
import { body, param } from "express-validator"; updateRisksStatus
import { createInstallationSession, getCurrentOrganizationInstallationStatus, getRisksForOrganization, linkInstallationToOrganization, updateRisksStatus } from "../../../controllers/v1/secretScanningController"; } from "../../../controllers/v1/secretScanningController";
import { ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER } from "../../../variables"; import { AuthMode } from "../../../variables";
router.post( router.post(
"/create-installation-session/organization/:organizationId", "/create-installation-session/organization/:organizationId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
param("organizationId").exists().trim(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
createInstallationSession createInstallationSession
); );
router.post( router.post(
"/link-installation", "/link-installation",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
body("installationId").exists().trim(),
body("sessionId").exists().trim(),
validateRequest,
linkInstallationToOrganization linkInstallationToOrganization
); );
router.get( router.get(
"/installation-status/organization/:organizationId", "/installation-status/organization/:organizationId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
param("organizationId").exists().trim(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
getCurrentOrganizationInstallationStatus getCurrentOrganizationInstallationStatus
); );
router.get( router.get(
"/organization/:organizationId/risks", "/organization/:organizationId/risks",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
param("organizationId").exists().trim(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
getRisksForOrganization getRisksForOrganization
); );
router.post( router.post(
"/organization/:organizationId/risks/:riskId/status", "/organization/:organizationId/risks/:riskId/status",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
param("organizationId").exists().trim(),
param("riskId").exists().trim(),
body("status").exists(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
updateRisksStatus updateRisksStatus
); );
+22 -73
View File
@@ -1,37 +1,22 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import passport from "passport"; import passport from "passport";
import { import { requireAuth } from "../../../middleware";
AuthProvider
} from "../../models";
import {
requireAuth,
requireOrganizationAuth,
validateRequest,
} from "../../../middleware";
import { body, query } from "express-validator";
import { ssoController } from "../../controllers/v1"; import { ssoController } from "../../controllers/v1";
import { authLimiter } from "../../../helpers/rateLimiter"; import { authLimiter } from "../../../helpers/rateLimiter";
import { import { AuthMode } from "../../../variables";
ACCEPTED,
ADMIN,
AuthMode,
OWNER
} from "../../../variables";
router.get( router.get("/redirect/google", authLimiter, (req, res, next) => {
"/redirect/google",
authLimiter,
(req, res, next) => {
passport.authenticate("google", { passport.authenticate("google", {
scope: ["profile", "email"], scope: ["profile", "email"],
session: false, session: false,
...(req.query.callback_port ? { ...(req.query.callback_port
? {
state: req.query.callback_port as string state: req.query.callback_port as string
} : {})
})(req, res, next);
} }
); : {})
})(req, res, next);
});
router.get( router.get(
"/google", "/google",
@@ -42,18 +27,16 @@ router.get(
ssoController.redirectSSO ssoController.redirectSSO
); );
router.get( router.get("/redirect/github", authLimiter, (req, res, next) => {
"/redirect/github",
authLimiter,
(req, res, next) => {
passport.authenticate("github", { passport.authenticate("github", {
session: false, session: false,
...(req.query.callback_port ? { ...(req.query.callback_port
? {
state: req.query.callback_port as string state: req.query.callback_port as string
} : {})
})(req, res, next);
} }
); : {})
})(req, res, next);
});
router.get( router.get(
"/github", "/github",
@@ -65,21 +48,18 @@ router.get(
ssoController.redirectSSO ssoController.redirectSSO
); );
router.get( router.get("/redirect/saml2/:ssoIdentifier", authLimiter, (req, res, next) => {
"/redirect/saml2/:ssoIdentifier",
authLimiter,
(req, res, next) => {
const options = { const options = {
failureRedirect: "/", failureRedirect: "/",
additionalParams: { additionalParams: {
RelayState: req.query.callback_port ?? "" RelayState: req.query.callback_port ?? ""
}, }
}; };
passport.authenticate("saml", options)(req, res, next); passport.authenticate("saml", options)(req, res, next);
} });
);
router.post("/saml2/:ssoIdentifier", router.post(
"/saml2/:ssoIdentifier",
passport.authenticate("saml", { passport.authenticate("saml", {
failureRedirect: "/login/provider/error", failureRedirect: "/login/provider/error",
failureFlash: true, failureFlash: true,
@@ -91,55 +71,24 @@ router.post("/saml2/:ssoIdentifier",
router.get( router.get(
"/config", "/config",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "query"
}),
query("organizationId").exists().trim(),
validateRequest,
ssoController.getSSOConfig ssoController.getSSOConfig
); );
router.post( router.post(
"/config", "/config",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "body"
}),
body("organizationId").exists().trim(),
body("authProvider").exists().isString().isIn([AuthProvider.OKTA_SAML]),
body("isActive").exists().isBoolean(),
body("entryPoint").exists().isString(),
body("issuer").exists().isString(),
body("cert").exists().isString(),
validateRequest,
ssoController.createSSOConfig ssoController.createSSOConfig
); );
router.patch( router.patch(
"/config", "/config",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "body"
}),
body("organizationId").exists().trim(),
body("authProvider").optional().isString(),
body("isActive").optional().isBoolean(),
body("entryPoint").optional().isString(),
body("issuer").optional().isString(),
body("cert").optional().isString(),
validateRequest,
ssoController.updateSSOConfig ssoController.updateSSOConfig
); );
+1 -4
View File
@@ -10,11 +10,8 @@ import { AuthMode } from "../../variables";
router.post( router.post(
"/signup", "/signup",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
body("inviteeEmail").exists().trim().notEmpty().isEmail(),
body("organizationId").exists().trim().notEmpty(),
validateRequest,
membershipOrgController.inviteUserToOrganization membershipOrgController.inviteUserToOrganization
); );
+12 -8
View File
@@ -9,40 +9,44 @@ import { AuthMode } from "../../variables";
// note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId) // note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId)
// TODO endpoint: consider moving these endpoints to be under /workspace to be more RESTful // TODO endpoint: consider moving these endpoints to be under /workspace to be more RESTful
router.get( // TODO endpoint: deprecate - used for old CLI (deprecate) router.get(
// TODO endpoint: deprecate - used for old CLI (deprecate)
"/:workspaceId/connect", "/:workspaceId/connect",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
param("workspaceId").exists().trim(), param("workspaceId").exists().trim(),
validateRequest, validateRequest,
membershipController.validateMembership membershipController.validateMembership
); );
router.delete( // TODO endpoint: check dashboard router.delete(
// TODO endpoint: check dashboard
"/:membershipId", "/:membershipId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
param("membershipId").exists().trim(), param("membershipId").exists().trim(),
validateRequest, validateRequest,
membershipController.deleteMembership membershipController.deleteMembership
); );
router.post( // TODO endpoint: check dashboard router.post(
// TODO endpoint: check dashboard
"/:membershipId/change-role", "/:membershipId/change-role",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
body("role").exists().trim(), body("role").exists().trim(),
validateRequest, validateRequest,
membershipController.changeMembershipRole membershipController.changeMembershipRole
); );
router.post( // TODO endpoint: check dashboard router.post(
// TODO endpoint: check dashboard
"/:membershipId/deny-permissions", "/:membershipId/deny-permissions",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
param("membershipId").isMongoId().exists().trim(), param("membershipId").isMongoId().exists().trim(),
body("permissions").isArray().exists(), body("permissions").isArray().exists(),
+4 -5
View File
@@ -5,10 +5,11 @@ import { requireAuth, validateRequest } from "../../middleware";
import { membershipOrgController } from "../../controllers/v1"; import { membershipOrgController } from "../../controllers/v1";
import { AuthMode } from "../../variables"; import { AuthMode } from "../../variables";
router.post( // TODO endpoint: check dashboard router.post(
// TODO endpoint: check dashboard
"/membershipOrg/:membershipOrgId/change-role", "/membershipOrg/:membershipOrgId/change-role",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
param("membershipOrgId"), param("membershipOrgId"),
validateRequest, validateRequest,
@@ -18,10 +19,8 @@ router.post( // TODO endpoint: check dashboard
router.delete( router.delete(
"/:membershipOrgId", // TODO endpoint: check dashboard "/:membershipOrgId", // TODO endpoint: check dashboard
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
param("membershipOrgId").exists().trim(),
validateRequest,
membershipOrgController.deleteMembershipOrg membershipOrgController.deleteMembershipOrg
); );
+20 -87
View File
@@ -1,150 +1,90 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { body, param } from "express-validator"; import { requireAuth } from "../../middleware";
import { import { AuthMode } from "../../variables";
requireAuth,
requireOrganizationAuth,
validateRequest,
} from "../../middleware";
import {
ACCEPTED,
ADMIN,
AuthMode,
MEMBER,
OWNER
} from "../../variables";
import { organizationController } from "../../controllers/v1"; import { organizationController } from "../../controllers/v1";
router.get( // TODO endpoint: deprecate (moved to api/v2/users/me/organizations) router.get(
// TODO endpoint: deprecate (moved to api/v2/users/me/organizations)
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
organizationController.getOrganizations organizationController.getOrganizations
); );
router.post( // not used on frontend router.post(
// not used on frontend
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
body("organizationName").exists().trim().notEmpty(),
validateRequest,
organizationController.createOrganization organizationController.createOrganization
); );
router.get( router.get(
"/:organizationId", "/:organizationId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationController.getOrganization organizationController.getOrganization
); );
router.get( // TODO endpoint: deprecate (moved to api/v2/organizations/:organizationId/memberships) router.get(
// TODO endpoint: deprecate (moved to api/v2/organizations/:organizationId/memberships)
"/:organizationId/users", "/:organizationId/users",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationController.getOrganizationMembers organizationController.getOrganizationMembers
); );
router.get( // TODO endpoint: move to /v2/users/me/organizations/:organizationId/workspaces router.get(
// TODO endpoint: move to /v2/users/me/organizations/:organizationId/workspaces
"/:organizationId/my-workspaces", // deprecated (moved to api/v2/organizations/:organizationId/workspaces) "/:organizationId/my-workspaces", // deprecated (moved to api/v2/organizations/:organizationId/workspaces)
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationController.getOrganizationWorkspaces organizationController.getOrganizationWorkspaces
); );
router.patch( router.patch(
"/:organizationId/name", "/:organizationId/name",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("name").exists().trim().notEmpty(),
validateRequest,
organizationController.changeOrganizationName organizationController.changeOrganizationName
); );
router.get( router.get(
"/:organizationId/incidentContactOrg", "/:organizationId/incidentContactOrg",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationController.getOrganizationIncidentContacts organizationController.getOrganizationIncidentContacts
); );
router.post( router.post(
"/:organizationId/incidentContactOrg", "/:organizationId/incidentContactOrg",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("email").exists().trim().notEmpty(),
validateRequest,
organizationController.addOrganizationIncidentContact organizationController.addOrganizationIncidentContact
); );
router.delete( router.delete(
"/:organizationId/incidentContactOrg", "/:organizationId/incidentContactOrg",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("email").exists().trim().notEmpty(),
validateRequest,
organizationController.deleteOrganizationIncidentContact organizationController.deleteOrganizationIncidentContact
); );
router.post( router.post(
"/:organizationId/customer-portal-session", // TODO endpoint: move to EE "/:organizationId/customer-portal-session", // TODO endpoint: move to EE
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationController.createOrganizationPortalSession organizationController.createOrganizationPortalSession
); );
@@ -153,14 +93,7 @@ router.get(
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationController.getOrganizationMembersAndTheirWorkspaces organizationController.getOrganizationMembersAndTheirWorkspaces
); );
export default router; export default router;
+5 -44
View File
@@ -2,93 +2,54 @@ import express from "express";
const router = express.Router(); const router = express.Router();
import { import {
requireAuth, requireAuth,
requireMembershipOrgAuth, requireOrganizationAuth
requireOrganizationAuth,
validateRequest
} from "../../middleware"; } from "../../middleware";
import { body, param } from "express-validator"; import { ACCEPTED, ADMIN, AuthMode, OWNER } from "../../variables";
import { ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER } from "../../variables";
import { organizationsController } from "../../controllers/v2"; import { organizationsController } from "../../controllers/v2";
// TODO: /POST to create membership // TODO: /POST to create membership
router.get( router.get(
"/:organizationId/memberships", "/:organizationId/memberships",
param("organizationId").exists().trim(),
validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
organizationsController.getOrganizationMemberships organizationsController.getOrganizationMemberships
); );
router.patch( router.patch(
"/:organizationId/memberships/:membershipId", "/:organizationId/memberships/:membershipId",
param("organizationId").exists().trim(),
param("membershipId").exists().trim(),
body("role").exists().isString().trim(),
validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
requireMembershipOrgAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
organizationsController.updateOrganizationMembership organizationsController.updateOrganizationMembership
); );
router.delete( router.delete(
"/:organizationId/memberships/:membershipId", "/:organizationId/memberships/:membershipId",
param("organizationId").exists().trim(),
param("membershipId").exists().trim(),
validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
requireMembershipOrgAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
organizationsController.deleteOrganizationMembership organizationsController.deleteOrganizationMembership
); );
router.get( router.get(
"/:organizationId/workspaces", "/:organizationId/workspaces",
param("organizationId").exists().trim(),
validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}), }),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
organizationsController.getOrganizationWorkspaces organizationsController.getOrganizationWorkspaces
); );
router.get( // TODO endpoint: deprecate service accounts router.get(
// TODO endpoint: deprecate service accounts
"/:organizationId/service-accounts", "/:organizationId/service-accounts",
param("organizationId").exists().trim(),
validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN], acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED], acceptedStatuses: [ACCEPTED]
}), }),
organizationsController.getOrganizationServiceAccounts organizationsController.getOrganizationServiceAccounts
); );
+32 -22
View File
@@ -2,6 +2,7 @@ import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@ca
import { MembershipOrg } from "../models"; import { MembershipOrg } from "../models";
import { IRole } from "../models/role"; import { IRole } from "../models/role";
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
import { ACCEPTED } from "../variables";
export enum GeneralPermissionActions { export enum GeneralPermissionActions {
Read = "read", Read = "read",
@@ -10,34 +11,37 @@ export enum GeneralPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum WorkspacePermissionActions {
Read = "read",
Create = "create"
}
export enum OrgPermissionSubjects { export enum OrgPermissionSubjects {
Workspace = "workspace", Workspace = "workspace",
Role = "role", Role = "role",
Member = "member", Member = "member",
Settings = "settings", Settings = "settings",
ServiceAccount = "service-account",
IncidentAccount = "incident-contact", IncidentAccount = "incident-contact",
Sso = "sso", Sso = "sso",
Billing = "billing" Billing = "billing",
SecretScanning = "secret-scanning"
} }
export type OrgPermissionSet = export type OrgPermissionSet =
| [GeneralPermissionActions, OrgPermissionSubjects.Workspace] | [WorkspacePermissionActions, OrgPermissionSubjects.Workspace]
| [GeneralPermissionActions, OrgPermissionSubjects.Role] | [GeneralPermissionActions, OrgPermissionSubjects.Role]
| [GeneralPermissionActions, OrgPermissionSubjects.Member] | [GeneralPermissionActions, OrgPermissionSubjects.Member]
| [GeneralPermissionActions, OrgPermissionSubjects.Settings] | [GeneralPermissionActions, OrgPermissionSubjects.Settings]
| [GeneralPermissionActions, OrgPermissionSubjects.ServiceAccount]
| [GeneralPermissionActions, OrgPermissionSubjects.IncidentAccount] | [GeneralPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [GeneralPermissionActions, OrgPermissionSubjects.Sso] | [GeneralPermissionActions, OrgPermissionSubjects.Sso]
| [GeneralPermissionActions, OrgPermissionSubjects.SecretScanning]
| [GeneralPermissionActions, OrgPermissionSubjects.Billing]; | [GeneralPermissionActions, OrgPermissionSubjects.Billing];
const buildAdminPermission = () => { const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
// ws permissions // ws permissions
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Workspace); can(WorkspacePermissionActions.Read, OrgPermissionSubjects.Workspace);
can(GeneralPermissionActions.Create, OrgPermissionSubjects.Workspace); can(WorkspacePermissionActions.Create, OrgPermissionSubjects.Workspace);
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.Workspace);
can(GeneralPermissionActions.Delete, OrgPermissionSubjects.Workspace);
// role permission // role permission
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Role); can(GeneralPermissionActions.Read, OrgPermissionSubjects.Role);
can(GeneralPermissionActions.Create, OrgPermissionSubjects.Role); can(GeneralPermissionActions.Create, OrgPermissionSubjects.Role);
@@ -49,16 +53,16 @@ const buildAdminPermission = () => {
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.Member); can(GeneralPermissionActions.Edit, OrgPermissionSubjects.Member);
can(GeneralPermissionActions.Delete, OrgPermissionSubjects.Member); can(GeneralPermissionActions.Delete, OrgPermissionSubjects.Member);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
can(GeneralPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(GeneralPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Settings); can(GeneralPermissionActions.Read, OrgPermissionSubjects.Settings);
can(GeneralPermissionActions.Create, OrgPermissionSubjects.Settings); can(GeneralPermissionActions.Create, OrgPermissionSubjects.Settings);
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.Settings); can(GeneralPermissionActions.Edit, OrgPermissionSubjects.Settings);
can(GeneralPermissionActions.Delete, OrgPermissionSubjects.Settings); can(GeneralPermissionActions.Delete, OrgPermissionSubjects.Settings);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.ServiceAccount);
can(GeneralPermissionActions.Create, OrgPermissionSubjects.ServiceAccount);
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.ServiceAccount);
can(GeneralPermissionActions.Delete, OrgPermissionSubjects.ServiceAccount);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); can(GeneralPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(GeneralPermissionActions.Create, OrgPermissionSubjects.IncidentAccount); can(GeneralPermissionActions.Create, OrgPermissionSubjects.IncidentAccount);
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.IncidentAccount); can(GeneralPermissionActions.Edit, OrgPermissionSubjects.IncidentAccount);
@@ -82,14 +86,15 @@ export const adminPermissions = buildAdminPermission();
const buildMemberPermission = () => { const buildMemberPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Workspace); can(WorkspacePermissionActions.Read, OrgPermissionSubjects.Workspace);
can(WorkspacePermissionActions.Create, OrgPermissionSubjects.Workspace);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Member); can(GeneralPermissionActions.Read, OrgPermissionSubjects.Member);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Role); can(GeneralPermissionActions.Read, OrgPermissionSubjects.Role);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Settings); can(GeneralPermissionActions.Read, OrgPermissionSubjects.Settings);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Billing); can(GeneralPermissionActions.Read, OrgPermissionSubjects.Billing);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Sso); can(GeneralPermissionActions.Read, OrgPermissionSubjects.Sso);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); can(GeneralPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(GeneralPermissionActions.Read, OrgPermissionSubjects.ServiceAccount); can(GeneralPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
return build(); return build();
}; };
@@ -98,23 +103,28 @@ export const memberPermissions = buildMemberPermission();
export const getUserOrgPermissions = async (userId: string, orgId: string) => { export const getUserOrgPermissions = async (userId: string, orgId: string) => {
// TODO(akhilmhdh): speed this up by pulling from cache later // TODO(akhilmhdh): speed this up by pulling from cache later
const orgMembership = await MembershipOrg.findOne({ user: userId, organization: orgId }) const membership = await MembershipOrg.findOne({
user: userId,
organization: orgId,
status: ACCEPTED
})
.populate<{ customRole: IRole & { permissions: RawRuleOf<MongoAbility<OrgPermissionSet>>[] } }>( .populate<{ customRole: IRole & { permissions: RawRuleOf<MongoAbility<OrgPermissionSet>>[] } }>(
"customRole" "customRole"
) )
.exec(); .exec();
if (!orgMembership || (orgMembership.role === "custom" && !orgMembership.customRole)) { if (!membership || (membership.role === "custom" && !membership.customRole)) {
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" }); throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
} }
if (orgMembership.role === "admin" || orgMembership.role === "owner") return adminPermissions; if (membership.role === "admin" || membership.role === "owner")
return { permission: adminPermissions, membership };
if (orgMembership.role === "member") return memberPermissions; if (membership.role === "member") return { permission: memberPermissions, membership };
if (orgMembership.role === "custom") { if (membership.role === "custom") {
const permission = createMongoAbility<OrgPermissionSet>(orgMembership.customRole.permissions); const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions);
return permission; return { permission, membership };
} }
throw BadRequestError({ message: "User role not found" }); throw BadRequestError({ message: "User role not found" });
+32 -16
View File
@@ -1,16 +1,10 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import { MembershipOrg } from "../models";
MembershipOrg, import { validateMembershipOrg } from "../helpers/membershipOrg";
} from "../models"; import { MembershipOrgNotFoundError, UnauthorizedRequestError } from "../utils/errors";
import {
validateMembershipOrg,
} from "../helpers/membershipOrg";
import {
MembershipOrgNotFoundError,
UnauthorizedRequestError,
} from "../utils/errors";
import { AuthData } from "../interfaces/middleware"; import { AuthData } from "../interfaces/middleware";
import { ActorType } from "../ee/models"; import { ActorType } from "../ee/models";
import { z } from "zod";
/** /**
* Validate authenticated clients for organization membership with id [membershipOrgId] based * Validate authenticated clients for organization membership with id [membershipOrgId] based
@@ -25,7 +19,7 @@ export const validateClientForMembershipOrg = async ({
authData, authData,
membershipOrgId, membershipOrgId,
acceptedRoles, acceptedRoles,
acceptedStatuses, acceptedStatuses
}: { }: {
authData: AuthData; authData: AuthData;
membershipOrgId: Types.ObjectId; membershipOrgId: Types.ObjectId;
@@ -34,8 +28,9 @@ export const validateClientForMembershipOrg = async ({
}) => { }) => {
const membershipOrg = await MembershipOrg.findById(membershipOrgId); const membershipOrg = await MembershipOrg.findById(membershipOrgId);
if (!membershipOrg) throw MembershipOrgNotFoundError({ if (!membershipOrg)
message: "Failed to find organization membership ", throw MembershipOrgNotFoundError({
message: "Failed to find organization membership "
}); });
switch (authData.actor.type) { switch (authData.actor.type) {
@@ -44,13 +39,34 @@ export const validateClientForMembershipOrg = async ({
userId: authData.authPayload._id, userId: authData.authPayload._id,
organizationId: membershipOrg.organization, organizationId: membershipOrg.organization,
acceptedRoles, acceptedRoles,
acceptedStatuses, acceptedStatuses
}); });
return membershipOrg; return membershipOrg;
case ActorType.SERVICE: case ActorType.SERVICE:
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: "Failed service account client authorization for organization membership", 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()
})
});
+156 -12
View File
@@ -1,12 +1,7 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import { z } from "zod";
IUser, import { IUser, Organization } from "../models";
Organization, import { OrganizationNotFoundError, UnauthorizedRequestError } from "../utils/errors";
} from "../models";
import {
OrganizationNotFoundError,
UnauthorizedRequestError,
} from "../utils/errors";
import { validateUserClientForOrganization } from "./user"; import { validateUserClientForOrganization } from "./user";
import { AuthData } from "../interfaces/middleware"; import { AuthData } from "../interfaces/middleware";
import { ActorType } from "../ee/models"; import { ActorType } from "../ee/models";
@@ -21,7 +16,7 @@ export const validateClientForOrganization = async ({
authData, authData,
organizationId, organizationId,
acceptedRoles, acceptedRoles,
acceptedStatuses, acceptedStatuses
}: { }: {
authData: AuthData; authData: AuthData;
organizationId: Types.ObjectId; organizationId: Types.ObjectId;
@@ -32,7 +27,7 @@ export const validateClientForOrganization = async ({
if (!organization) { if (!organization) {
throw OrganizationNotFoundError({ throw OrganizationNotFoundError({
message: "Failed to find organization", message: "Failed to find organization"
}); });
} }
@@ -43,13 +38,162 @@ export const validateClientForOrganization = async ({
user: authData.authPayload as IUser, user: authData.authPayload as IUser,
organization, organization,
acceptedRoles, acceptedRoles,
acceptedStatuses, acceptedStatuses
}); });
return { organization, membershipOrg }; return { organization, membershipOrg };
case ActorType.SERVICE: case ActorType.SERVICE:
throw UnauthorizedRequestError({ 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()
})
});
+29 -36
View File
@@ -1,25 +1,14 @@
import net from "net"; import net from "net";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import { IServiceTokenData, IUser, SecretBlindIndexData, Workspace } from "../models";
IServiceTokenData, import { ActorType, TrustedIP } from "../ee/models";
IUser,
SecretBlindIndexData,
Workspace,
} from "../models";
import {
ActorType,
TrustedIP
} from "../ee/models";
import { validateUserClientForWorkspace } from "./user"; import { validateUserClientForWorkspace } from "./user";
import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData";
import { import { BadRequestError, UnauthorizedRequestError, WorkspaceNotFoundError } from "../utils/errors";
BadRequestError,
UnauthorizedRequestError,
WorkspaceNotFoundError,
} from "../utils/errors";
import { BotService } from "../services"; import { BotService } from "../services";
import { AuthData } from "../interfaces/middleware"; import { AuthData } from "../interfaces/middleware";
import { extractIPDetails } from "../utils/ip"; import { extractIPDetails } from "../utils/ip";
import { z } from "zod";
/** /**
* Validate authenticated clients for workspace with id [workspaceId] based * Validate authenticated clients for workspace with id [workspaceId] based
@@ -52,8 +41,9 @@ export const validateClientForWorkspace = async ({
}) => { }) => {
const workspace = await Workspace.findById(workspaceId); const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw WorkspaceNotFoundError({ if (!workspace)
message: "Failed to find workspace", throw WorkspaceNotFoundError({
message: "Failed to find workspace"
}); });
if (requireBlindIndicesEnabled) { if (requireBlindIndicesEnabled) {
@@ -62,19 +52,21 @@ export const validateClientForWorkspace = async ({
// and no admin has enabled it) // and no admin has enabled it)
const secretBlindIndexData = await SecretBlindIndexData.exists({ const secretBlindIndexData = await SecretBlindIndexData.exists({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId)
}); });
if (!secretBlindIndexData) throw UnauthorizedRequestError({ if (!secretBlindIndexData)
message: "Failed workspace authorization due to blind indices not being enabled", throw UnauthorizedRequestError({
message: "Failed workspace authorization due to blind indices not being enabled"
}); });
} }
if (requireE2EEOff) { if (requireE2EEOff) {
const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId); const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId);
if (isWorkspaceE2EE) throw BadRequestError({ if (isWorkspaceE2EE)
message: "Failed workspace authorization due to end-to-end encryption not being disabled", throw BadRequestError({
message: "Failed workspace authorization due to end-to-end encryption not being disabled"
}); });
} }
@@ -86,10 +78,10 @@ export const validateClientForWorkspace = async ({
workspaceId, workspaceId,
environment, environment,
acceptedRoles, acceptedRoles,
requiredPermissions, requiredPermissions
}); });
return ({ membership, workspace }); return { membership, workspace };
case ActorType.SERVICE: case ActorType.SERVICE:
if (checkIPAllowlist) { if (checkIPAllowlist) {
const trustedIps = await TrustedIP.find({ const trustedIps = await TrustedIP.find({
@@ -103,23 +95,17 @@ export const validateClientForWorkspace = async ({
for (const trustedIp of trustedIps) { for (const trustedIp of trustedIps) {
if (trustedIp.prefix !== undefined) { if (trustedIp.prefix !== undefined) {
blockList.addSubnet( blockList.addSubnet(trustedIp.ipAddress, trustedIp.prefix, trustedIp.type);
trustedIp.ipAddress,
trustedIp.prefix,
trustedIp.type
);
} else { } else {
blockList.addAddress( blockList.addAddress(trustedIp.ipAddress, trustedIp.type);
trustedIp.ipAddress,
trustedIp.type
);
} }
} }
const { type } = extractIPDetails(authData.ipAddress); const { type } = extractIPDetails(authData.ipAddress);
const check = blockList.check(authData.ipAddress, type); const check = blockList.check(authData.ipAddress, type);
if (!check) throw UnauthorizedRequestError({ if (!check)
throw UnauthorizedRequestError({
message: "Failed workspace authorization" message: "Failed workspace authorization"
}); });
} }
@@ -129,9 +115,16 @@ export const validateClientForWorkspace = async ({
serviceTokenData: authData.authPayload as IServiceTokenData, serviceTokenData: authData.authPayload as IServiceTokenData,
workspaceId, workspaceId,
environment, environment,
requiredPermissions, requiredPermissions
}); });
return {}; return {};
} }
} };
export const CreateWorkspacev1 = z.object({
body: z.object({
workspaceName: z.string().trim(),
organizationId: z.string().trim()
})
});
+10 -2
View File
@@ -15,10 +15,18 @@ export type TRole = {
updatedAt: string; updatedAt: string;
}; };
export type TPermission = { export type TPermission = TWorkspacePermission | TGeneralPermission;
type TGeneralPermission = {
condition?: Record<string, any>; condition?: Record<string, any>;
action: "read" | "edit" | "create" | "delete"; action: "read" | "edit" | "create" | "delete";
subject: string; subject: "member" | "role" | "incident-contact" | "sso" | "billing" | "settings";
};
type TWorkspacePermission = {
condition?: Record<string, any>;
action: "read" | "create";
subject: "workspace";
}; };
export type TCreateRoleDTO = { export type TCreateRoleDTO = {
@@ -20,7 +20,7 @@ import {
TFormSchema TFormSchema
} from "./OrgRoleModifySection.utils"; } from "./OrgRoleModifySection.utils";
import { RolePermission } from "./RolePermission"; import { RolePermission } from "./RolePermission";
import { ServiceAccountPermission } from "./ServiceAccountPermission"; import { SecretScannigPermission } from "./SecretScanningPermission";
import { SettingsPermission } from "./SettingsPermission"; import { SettingsPermission } from "./SettingsPermission";
import { SsoPermission } from "./SsoPermission"; import { SsoPermission } from "./SsoPermission";
import { WorkspacePermission } from "./WorkspacePermission"; import { WorkspacePermission } from "./WorkspacePermission";
@@ -183,15 +183,15 @@ export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
/> />
</div> </div>
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<SsoPermission isNonEditable={isNonEditable} control={control} setValue={setValue} /> <SecretScannigPermission
</div>
<div className="flex flex-col space-y-4">
<ServiceAccountPermission
isNonEditable={isNonEditable} isNonEditable={isNonEditable}
control={control} control={control}
setValue={setValue} setValue={setValue}
/> />
</div> </div>
<div className="flex flex-col space-y-4">
<SsoPermission isNonEditable={isNonEditable} control={control} setValue={setValue} />
</div>
</div> </div>
<div className="flex items-center space-x-4 mt-12"> <div className="flex items-center space-x-4 mt-12">
<Button <Button
@@ -3,8 +3,6 @@ import { z } from "zod";
import { TPermission } from "@app/hooks/api/roles/types"; import { TPermission } from "@app/hooks/api/roles/types";
const PERMISSION_ACTIONS = ["read", "create", "edit", "delete"] as const;
const generalPermissionSchema = z.object({ const generalPermissionSchema = z.object({
read: z.boolean().optional(), read: z.boolean().optional(),
edit: z.boolean().optional(), edit: z.boolean().optional(),
@@ -17,12 +15,16 @@ export const formSchema = z.object({
description: z.string().optional(), description: z.string().optional(),
slug: z.string(), slug: z.string(),
permissions: z.object({ permissions: z.object({
workspace: z.record(generalPermissionSchema), workspace: z.object({
read: z.boolean().optional(),
create: z.boolean().optional()
}),
member: generalPermissionSchema, member: generalPermissionSchema,
role: generalPermissionSchema, role: generalPermissionSchema,
settings: generalPermissionSchema, settings: generalPermissionSchema,
"service-account": generalPermissionSchema, "service-account": generalPermissionSchema,
"incident-contact": generalPermissionSchema, "incident-contact": generalPermissionSchema,
"secret-scanning": generalPermissionSchema,
sso: generalPermissionSchema, sso: generalPermissionSchema,
billing: generalPermissionSchema billing: generalPermissionSchema
}) })
@@ -30,25 +32,6 @@ export const formSchema = z.object({
export type TFormSchema = z.infer<typeof formSchema>; export type TFormSchema = z.infer<typeof formSchema>;
const api2FormWorkspace = (
formVal: TFormSchema["permissions"]["workspace"],
permission: TPermission
) => {
if (permission.subject !== "workspace") return;
const isCustomRule = Boolean(permission?.condition?.id);
// full access
if (isCustomRule && !formVal?.custom) {
formVal.custom = { read: true, edit: true, delete: true, create: true };
}
const workspaceId = permission?.condition?.id || "all";
// initalize
if (!formVal?.[workspaceId]) {
formVal[workspaceId] = { read: false, edit: false, create: false, delete: false };
}
formVal[workspaceId][permission.action] = true;
};
// convert role permission to form compatiable data structure // convert role permission to form compatiable data structure
export const rolePermission2Form = (permissions: TPermission[] = []) => { export const rolePermission2Form = (permissions: TPermission[] = []) => {
const formVal: TFormSchema["permissions"] = { const formVal: TFormSchema["permissions"] = {
@@ -59,68 +42,31 @@ export const rolePermission2Form = (permissions: TPermission[] = []) => {
sso: {}, sso: {},
member: {}, member: {},
"service-account": {}, "service-account": {},
"incident-contact": {} "incident-contact": {},
"secret-scanning": {}
}; };
permissions.forEach((permission) => { permissions.forEach((permission) => {
switch (permission.subject) { // akhilmhdh: this is typecast as workspace key else i would need an if loop with same condition on both side
case "workspace": formVal[permission.subject][permission.action as keyof typeof formVal.workspace] = true;
api2FormWorkspace(formVal?.workspace, permission);
break;
default:
// everything else follows same pattern
// formVal[settings][read | write] = true
formVal[permission.subject as keyof TFormSchema["permissions"]][permission.action] = true;
break;
}
}); });
return formVal; return formVal;
}; };
const form2ApiWorkspace = (
permissions: TPermission[],
workspace: TFormSchema["permissions"]["workspace"]
) => {
const isFullAccess = PERMISSION_ACTIONS.every((action) => workspace?.all?.[action]);
// if any of them is set in all push it without any condition
PERMISSION_ACTIONS.forEach((action) => {
if (workspace?.all?.[action]) permissions.push({ action, subject: "workspace" });
});
if (!isFullAccess) {
Object.keys(workspace)
.filter((id) => id !== "all" && id !== "custom") // remove all and custom for iter
.forEach((workspaceId) => {
const actions = Object.keys(workspace[workspaceId]) as ["read", "edit", "create", "delete"];
actions.forEach((action) => {
// if not full access for an action
if (!workspace?.all?.[action] && workspace[workspaceId][action]) {
permissions.push({ action, subject: "workspace", condition: { id: workspaceId } });
}
});
});
}
};
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => { export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
const permissions: TPermission[] = []; const permissions: TPermission[] = [];
if (formVal?.workspace) { (Object.keys(formVal) as Array<keyof typeof formVal>).forEach((rule) => {
// easy deep copy
form2ApiWorkspace(permissions, JSON.parse(JSON.stringify(formVal.workspace)));
}
// other than workspace everything else follows same
// if in future there is a different follow the above on how workspace is done
const { workspace, ...rules } = formVal;
(Object.keys(rules) as Array<keyof typeof rules>).forEach((rule) => {
// all these type annotations are due to Object.keys of ts cannot infer and put it just a string[] // all these type annotations are due to Object.keys of ts cannot infer and put it just a string[]
// quite annoying i know // quite annoying i know
const actions = Object.keys(rules[rule]) as Array< const actions = Object.keys(formVal[rule]) as Array<
keyof z.infer<typeof generalPermissionSchema> keyof z.infer<typeof generalPermissionSchema>
>; >;
actions.forEach((action) => { actions.forEach((action) => {
if (rules[rule][action]) { // akhilmhdh: set it as any due to the union type bug i would end up writing an if else with same condition on both side
permissions.push({ action, subject: rule }); if (formVal[rule][action as keyof typeof formVal.workspace]) {
permissions.push({ subject: rule, action } as any);
} }
}); });
}); });
@@ -1,6 +1,6 @@
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faLaptopCode } from "@fortawesome/free-solid-svg-icons"; import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -30,10 +30,10 @@ const PERMISSIONS = [
{ action: "delete", label: "Remove" } { action: "delete", label: "Remove" }
] as const; ] as const;
export const ServiceAccountPermission = ({ isNonEditable, setValue, control }: Props) => { export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({ const rule = useWatch({
control, control,
name: "permissions.service-account" name: "permissions.secret-scanning"
}); });
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
@@ -60,7 +60,7 @@ export const ServiceAccountPermission = ({ isNonEditable, setValue, control }: P
case Permission.NoAccess: case Permission.NoAccess:
setIsCustom.off(); setIsCustom.off();
setValue( setValue(
"permissions.service-account", "permissions.secret-scanning",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
{ shouldDirty: true } { shouldDirty: true }
); );
@@ -68,7 +68,7 @@ export const ServiceAccountPermission = ({ isNonEditable, setValue, control }: P
case Permission.FullAccess: case Permission.FullAccess:
setIsCustom.off(); setIsCustom.off();
setValue( setValue(
"permissions.service-account", "permissions.secret-scanning",
{ read: true, edit: true, create: true, delete: true }, { read: true, edit: true, create: true, delete: true },
{ shouldDirty: true } { shouldDirty: true }
); );
@@ -76,7 +76,7 @@ export const ServiceAccountPermission = ({ isNonEditable, setValue, control }: P
case Permission.ReadOnly: case Permission.ReadOnly:
setIsCustom.off(); setIsCustom.off();
setValue( setValue(
"permissions.service-account", "permissions.secret-scanning",
{ read: true, edit: false, create: false, delete: false }, { read: true, edit: false, create: false, delete: false },
{ shouldDirty: true } { shouldDirty: true }
); );
@@ -84,7 +84,7 @@ export const ServiceAccountPermission = ({ isNonEditable, setValue, control }: P
default: default:
setIsCustom.on(); setIsCustom.on();
setValue( setValue(
"permissions.service-account", "permissions.secret-scanning",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
{ shouldDirty: true } { shouldDirty: true }
); );
@@ -101,11 +101,11 @@ export const ServiceAccountPermission = ({ isNonEditable, setValue, control }: P
> >
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div> <div>
<FontAwesomeIcon icon={faLaptopCode} className="text-4xl" /> <FontAwesomeIcon icon={faMagnifyingGlass} className="text-4xl" />
</div> </div>
<div className="flex-grow flex flex-col"> <div className="flex-grow flex flex-col">
<div className="font-medium mb-1 text-lg">Service Accounts</div> <div className="font-medium mb-1 text-lg">Secret Scanning</div>
<div className="text-xs font-light">Service Account management control</div> <div className="text-xs font-light">Secret scanning management control</div>
</div> </div>
<div> <div>
<Select <Select
@@ -129,14 +129,14 @@ export const ServiceAccountPermission = ({ isNonEditable, setValue, control }: P
{isCustom && {isCustom &&
PERMISSIONS.map(({ action, label }) => ( PERMISSIONS.map(({ action, label }) => (
<Controller <Controller
name={`permissions.service-account.${action}`} name={`permissions.role.${action}`}
key={`permissions.service-account.${action}`} key={`permissions.role.${action}`}
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<Checkbox <Checkbox
isChecked={field.value} isChecked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
id={`permissions.service-account.${action}`} id={`permissions.role.${action}`}
isDisabled={isNonEditable} isDisabled={isNonEditable}
> >
{label} {label}
@@ -1,23 +1,12 @@
import { useMemo } from "react"; import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faClipboardList } from "@fortawesome/free-solid-svg-icons"; import { faMoneyBill } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { import { Checkbox, Select, SelectItem } from "@app/components/v2";
Checkbox, import { useToggle } from "@app/hooks";
Select,
SelectItem,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { TFormSchema } from "./OrgRoleModifySection.utils"; import { TFormSchema } from "./OrgRoleModifySection.utils";
@@ -34,48 +23,50 @@ enum Permission {
Custom = "custom" Custom = "custom"
} }
export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props) => { const PERMISSIONS = [
const { workspaces } = useWorkspace(); { action: "read", label: "Read" },
{ action: "create", label: "Create" }
] as const;
const customWorkspaceRule = useWatch({ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({
control, control,
name: "permissions.workspace.custom" name: "permissions.workspace"
}); });
const isCustom = Boolean(customWorkspaceRule); const [isCustom, setIsCustom] = useToggle();
const allWorkspaceRule = useWatch({ control, name: "permissions.workspace.all" });
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
const { read, delete: del, edit, create } = allWorkspaceRule || {}; let score = 0;
if (read && del && edit && create) return Permission.FullAccess; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
if (read) return Permission.ReadOnly; const totalActions = PERMISSIONS.length;
return Permission.NoAccess; actions.forEach((key) => (score += rule[key] ? 1 : 0));
}, [allWorkspaceRule]);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule.read) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setValue("permissions.workspace", {}, { shouldDirty: true }); setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });
break; break;
case Permission.FullAccess: case Permission.FullAccess:
setValue( setValue("permissions.workspace", { read: true, create: true }, { shouldDirty: true });
"permissions.workspace",
{ all: { read: true, edit: true, create: true, delete: true } },
{ shouldDirty: true }
);
break; break;
case Permission.ReadOnly: case Permission.ReadOnly:
setValue( setValue("permissions.workspace", { read: true, create: false }, { shouldDirty: true });
"permissions.workspace",
{ all: { read: true, edit: false, create: false, delete: false } },
{ shouldDirty: true }
);
break; break;
default: default:
setValue( setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });
"permissions.workspace",
{ custom: { read: false, edit: false, create: false, delete: false } },
{ shouldDirty: true }
);
break; break;
} }
}; };
@@ -84,23 +75,22 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
<div <div
className={twMerge( className={twMerge(
"px-10 py-6 bg-mineshaft-800 rounded-md", "px-10 py-6 bg-mineshaft-800 rounded-md",
(selectedPermissionCategory !== Permission.NoAccess || isCustom) && selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
"border-l-2 border-primary-600"
)} )}
> >
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div> <div>
<FontAwesomeIcon icon={faClipboardList} className="text-4xl" /> <FontAwesomeIcon icon={faMoneyBill} className="text-4xl" />
</div> </div>
<div className="flex-grow flex flex-col"> <div className="flex-grow flex flex-col">
<div className="font-medium mb-1 text-lg">Projects</div> <div className="font-medium mb-1 text-lg">Project</div>
<div className="text-xs font-light">User project access control</div> <div className="text-xs font-light">Project management control</div>
</div> </div>
<div> <div>
<Select <Select
defaultValue={Permission.NoAccess} defaultValue={Permission.NoAccess}
isDisabled={isNonEditable} isDisabled={isNonEditable}
value={isCustom ? Permission.Custom : selectedPermissionCategory} value={selectedPermissionCategory}
onValueChange={handlePermissionChange} onValueChange={handlePermissionChange}
> >
<SelectItem value={Permission.NoAccess}>No Access</SelectItem> <SelectItem value={Permission.NoAccess}>No Access</SelectItem>
@@ -112,100 +102,27 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
</div> </div>
<motion.div <motion.div
initial={false} initial={false}
animate={{ height: isCustom ? "auto" : 0 }} animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="overflow-hidden" className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
> >
<TableContainer className="border-mineshaft-500 mt-6">
<Table>
<THead>
<Tr>
<Th />
<Th className="text-center">Read</Th>
<Th className="text-center">Create</Th>
<Th className="text-center">Edit</Th>
<Th className="text-center">Delete</Th>
</Tr>
</THead>
<TBody>
{isCustom && {isCustom &&
workspaces?.map(({ name, _id: id }) => ( PERMISSIONS.map(({ action, label }) => (
<Tr key={`custom-role-ws-${name}`}>
<Td>{name}</Td>
<Td>
<Controller <Controller
name={`permissions.workspace.${id}.read`} name={`permissions.workspace.${action}`}
key={`permissions.workspace.${action}`}
control={control} control={control}
defaultValue={false}
render={({ field }) => ( render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox <Checkbox
isChecked={field.value} isChecked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
id={`permissions.workspace.${id}.read`} id={`permissions.workspace.${action}`}
isDisabled={isNonEditable} isDisabled={isNonEditable}
/> >
</div> {label}
</Checkbox>
)} )}
/> />
</Td>
<Td>
<Controller
name={`permissions.workspace.${id}.create`}
control={control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
onBlur={field.onBlur}
id={`permissions.workspace.${id}.modify`}
isDisabled={isNonEditable}
/>
</div>
)}
/>
</Td>
<Td>
<Controller
name={`permissions.workspace.${id}.edit`}
control={control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
onBlur={field.onBlur}
id={`permissions.workspace.${id}.modify`}
isDisabled={isNonEditable}
/>
</div>
)}
/>
</Td>
<Td>
<Controller
defaultValue={false}
name={`permissions.workspace.${id}.delete`}
control={control}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.workspace.${id}.delete`}
isDisabled={isNonEditable}
/>
</div>
)}
/>
</Td>
</Tr>
))} ))}
</TBody>
</Table>
</TableContainer>
</motion.div> </motion.div>
</div> </div>
); );