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,22 +92,20 @@ 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({
organization: new Types.ObjectId(organizationId) organization: new Types.ObjectId(organizationId)
}); });
@@ -119,9 +113,8 @@ 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" });
});
} }
if (plan.memberLimit !== null) { if (plan.memberLimit !== null) {
@@ -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");
@@ -1,28 +1,38 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { import {
IncidentContactOrg, IncidentContactOrg,
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,28 +43,26 @@ 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,10 +72,23 @@ 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 {
return res.status(200).send({ params: { organizationId }
organization, } = 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({
organization
});
}; };
/** /**
@@ -77,15 +98,23 @@ 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,15 +147,15 @@ 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,25 +165,33 @@ 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,19 +200,24 @@ 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 },
@@ -192,9 +244,9 @@ export const addOrganizationIncidentContact = async (
{ upsert: true, new: true } { upsert: true, new: true }
); );
return res.status(200).send({ return res.status(200).send({
incidentContactOrg, incidentContactOrg
}); });
}; };
/** /**
@@ -203,22 +255,27 @@ 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,36 +349,43 @@ 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 workspacesSet = ( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
await Workspace.find( ForbiddenError.from(permission).throwUnlessCan(
{ GeneralPermissionActions.Read,
organization: organizationId, OrgPermissionSubjects.Member
}, );
"_id" ForbiddenError.from(permission).throwUnlessCan(
) WorkspacePermissionActions.Read,
).map((w) => w._id.toString()); OrgPermissionSubjects.Workspace
);
const memberships = ( const workspacesSet = (
await Membership.find({ await Workspace.find(
workspace: { $in: workspacesSet }, {
}).populate("workspace") organization: organizationId
); },
const userToWorkspaceIds: any = {}; "_id"
)
).map((w) => w._id.toString());
memberships.forEach(membership => { const memberships = await Membership.find({
const user = membership.user.toString(); workspace: { $in: workspacesSet }
if (userToWorkspaceIds[user]) { }).populate("workspace");
userToWorkspaceIds[user].push(membership.workspace); const userToWorkspaceIds: any = {};
} else {
userToWorkspaceIds[user] = [membership.workspace];
}
});
return res.json(userToWorkspaceIds); memberships.forEach((membership) => {
const user = membership.user.toString();
if (userToWorkspaceIds[user]) {
userToWorkspaceIds[user].push(membership.workspace);
} else {
userToWorkspaceIds[user] = [membership.workspace];
}
});
return res.json(userToWorkspaceIds);
}; };
+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, },
organizationId: installationSession.organization, {
user: installationSession.user installationId: installationId,
}, { organizationId: installationSession.organization,
upsert: true user: installationSession.user
}).lean() },
{
upsert: true
}
).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,24 +110,28 @@ 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) {
// case: limit imposed on number of workspaces allowed // case: limit imposed on number of workspaces allowed
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,228 +3,503 @@ 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 },
const { data } = await licenseServerKeyRequest.get( params: { organizationId }
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` } = await validateRequest(reqValidator.GetOrgPlansTablev1, req);
);
return res.status(200).send(data); const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
} ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
);
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
);
return res.status(200).send({ const plan = await EELicenseService.getPlan(
plan, new Types.ObjectId(organizationId),
}); new Types.ObjectId(workspaceId)
} );
return res.status(200).send({
plan
});
};
/** /**
* Return checkout url for pro trial * Return checkout url for pro trial
* @param req * @param req
* @param res * @param res
* @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,
success_url OrgPermissionSubjects.Billing
} );
); ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Edit,
EELicenseService.delPlan(new Types.ObjectId(organizationId)); OrgPermissionSubjects.Billing
);
return res.status(200).send({
url 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
}
);
EELicenseService.delPlan(new Types.ObjectId(organizationId));
return res.status(200).send({
url
});
};
/** /**
* Return the organization's current plan's billing info * Return the organization's current plan's billing info
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => { export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => {
const { data } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/billing` params: { organizationId }
); } = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
return res.status(200).send(data); 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(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/cloud-plan/billing`
);
return res.status(200).send(data);
};
/** /**
* Return the organization's current plan's feature table * Return the organization's current plan's feature table
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationPlanTable = async (req: Request, res: Response) => { export const getOrganizationPlanTable = async (req: Request, res: Response) => {
const { data } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/table` params: { organizationId }
); } = await validateRequest(reqValidator.GetOrgPlanTablev1, req);
return res.status(200).send(data); 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(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/cloud-plan/table`
);
return res.status(200).send(data);
};
export const getOrganizationBillingDetails = async (req: Request, res: Response) => { export const getOrganizationBillingDetails = async (req: Request, res: Response) => {
const { data } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details` params: { organizationId }
); } = await validateRequest(reqValidator.GetOrgBillingDetailsv1, req);
return res.status(200).send(data); 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(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details`
);
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 { data } = await licenseServerKeyRequest.patch( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details`, ForbiddenError.from(permission).throwUnlessCan(
{ GeneralPermissionActions.Edit,
...(name ? { name } : {}), OrgPermissionSubjects.Billing
...(email ? { email } : {}) );
}
);
return res.status(200).send(data); const organization = await Organization.findById(organizationId);
} if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details`,
{
...(name ? { name } : {}),
...(email ? { email } : {})
}
);
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);
return res.status(200).send(pmtMethods); 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 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,
success_url, OrgPermissionSubjects.Billing
cancel_url, );
}
); const organization = await Organization.findById(organizationId);
if (!organization) {
return res.status(200).send({ throw OrganizationNotFoundError({
url, 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,
cancel_url
}
);
return res.status(200).send({
url
});
};
/** /**
* Delete payment method with id [pmtMethodId] for organization * Delete payment method with id [pmtMethodId] for organization
* @param req * @param req
* @param res * @param res
* @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 { data } = await licenseServerKeyRequest.delete( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods/${pmtMethodId}`, ForbiddenError.from(permission).throwUnlessCan(
); GeneralPermissionActions.Delete,
OrgPermissionSubjects.Billing
return res.status(200).send(data); );
}
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods/${pmtMethodId}`
);
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);
return res.status(200).send(tax_ids); 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);
};
/** /**
* 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 { data } = await licenseServerKeyRequest.post( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids`, ForbiddenError.from(permission).throwUnlessCan(
{ GeneralPermissionActions.Create,
type, OrgPermissionSubjects.Billing
value );
}
);
return res.status(200).send(data); const organization = await Organization.findById(organizationId);
} if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/tax-ids`,
{
type,
value
}
);
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
* @param req * @param req
* @param res * @param res
* @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 { data } = await licenseServerKeyRequest.delete( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids/${taxId}`, ForbiddenError.from(permission).throwUnlessCan(
); GeneralPermissionActions.Delete,
OrgPermissionSubjects.Billing
return res.status(200).send(data); );
}
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/tax-ids/${taxId}`
);
return res.status(200).send(data);
};
/** /**
* Return organization's invoices on file * Return organization's invoices on file
* @param req * @param req
* @param res * @param res
* @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);
return res.status(200).send(invoices); 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 organization's licenses on file * Return organization's licenses on file
* @param req * @param req
* @param res * @param res
* @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);
return res.status(200).send(licenses); 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);
};
+211 -192
View File
@@ -2,239 +2,258 @@ 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
* to finish inputting their master key for logging in or signing up * to finish inputting their master key for logging in or signing up
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
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
* @param req * @param req
* @param res * @param res
* @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 }
const data = await getSSOConfigHelper({ } = await validateRequest(reqValidator.GetSsoConfigv1, req);
organizationId: new Types.ObjectId(organizationId)
});
return res.status(200).send(data); const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
} ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Read,
OrgPermissionSubjects.Sso
);
const data = await getSSOConfigHelper({
organizationId: new Types.ObjectId(organizationId)
});
return res.status(200).send(data);
};
/** /**
* Update organization SAML SSO configuration * Update organization SAML SSO configuration
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateSSOConfig = async (req: Request, res: Response) => { export const updateSSOConfig = async (req: Request, res: Response) => {
const {
body: { organizationId, authProvider, isActive, entryPoint, issuer, cert }
} = await validateRequest(reqValidator.UpdateSsoConfigv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Edit,
OrgPermissionSubjects.Sso
);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (!plan.samlSSO)
return res.status(400).send({
message:
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
});
interface PatchUpdate {
authProvider?: string;
isActive?: boolean;
encryptedEntryPoint?: string;
entryPointIV?: string;
entryPointTag?: string;
encryptedIssuer?: string;
issuerIV?: string;
issuerTag?: string;
encryptedCert?: string;
certIV?: string;
certTag?: string;
}
const update: PatchUpdate = {};
if (authProvider) {
update.authProvider = authProvider;
}
if (isActive !== undefined) {
update.isActive = isActive;
}
const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
if (entryPoint) {
const { const {
organizationId, ciphertext: encryptedEntryPoint,
authProvider, iv: entryPointIV,
isActive, tag: entryPointTag
entryPoint, } = client.encryptSymmetric(entryPoint, key);
issuer,
cert,
} = req.body;
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); update.encryptedEntryPoint = encryptedEntryPoint;
update.entryPointIV = entryPointIV;
if (!plan.samlSSO) return res.status(400).send({ update.entryPointTag = entryPointTag;
message: "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." }
if (issuer) {
const {
ciphertext: encryptedIssuer,
iv: issuerIV,
tag: issuerTag
} = client.encryptSymmetric(issuer, key);
update.encryptedIssuer = encryptedIssuer;
update.issuerIV = issuerIV;
update.issuerTag = issuerTag;
}
if (cert) {
const {
ciphertext: encryptedCert,
iv: certIV,
tag: certTag
} = client.encryptSymmetric(cert, key);
update.encryptedCert = encryptedCert;
update.certIV = certIV;
update.certTag = certTag;
}
const ssoConfig = await SSOConfig.findOneAndUpdate(
{
organization: new Types.ObjectId(organizationId)
},
update,
{
new: true
}
);
if (!ssoConfig)
throw ResourceNotFoundError({
message: "Failed to find SSO config to update"
}); });
interface PatchUpdate {
authProvider?: string;
isActive?: boolean;
encryptedEntryPoint?: string;
entryPointIV?: string;
entryPointTag?: string;
encryptedIssuer?: string;
issuerIV?: string;
issuerTag?: string;
encryptedCert?: string;
certIV?: string;
certTag?: string;
}
const update: PatchUpdate = {};
if (authProvider) {
update.authProvider = authProvider;
}
if (isActive !== undefined) {
update.isActive = isActive;
}
const key = await BotOrgService.getSymmetricKey(
new Types.ObjectId(organizationId)
);
if (entryPoint) {
const {
ciphertext: encryptedEntryPoint,
iv: entryPointIV,
tag: entryPointTag
} = client.encryptSymmetric(entryPoint, key);
update.encryptedEntryPoint = encryptedEntryPoint;
update.entryPointIV = entryPointIV;
update.entryPointTag = entryPointTag;
}
if (issuer) { if (update.isActive !== undefined) {
const { const membershipOrgs = await MembershipOrg.find({
ciphertext: encryptedIssuer, organization: new Types.ObjectId(organizationId)
iv: issuerIV, }).select("user");
tag: issuerTag
} = client.encryptSymmetric(issuer, key);
update.encryptedIssuer = encryptedIssuer;
update.issuerIV = issuerIV;
update.issuerTag = issuerTag;
}
if (cert) { if (update.isActive) {
const { await User.updateMany(
ciphertext: encryptedCert,
iv: certIV,
tag: certTag
} = client.encryptSymmetric(cert, key);
update.encryptedCert = encryptedCert;
update.certIV = certIV;
update.certTag = certTag;
}
const ssoConfig = await SSOConfig.findOneAndUpdate(
{ {
organization: new Types.ObjectId(organizationId) _id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
}
}, },
update,
{ {
new: true authMethods: [ssoConfig.authProvider]
} }
); );
} else {
if (!ssoConfig) throw ResourceNotFoundError({ await User.updateMany(
message: "Failed to find SSO config to update" {
}); _id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
if (update.isActive !== undefined) { }
const membershipOrgs = await MembershipOrg.find({ },
organization: new Types.ObjectId(organizationId) {
}).select("user"); authMethods: [AuthMethod.EMAIL]
if (update.isActive) {
await User.updateMany(
{
_id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
}
},
{
authMethods: [ssoConfig.authProvider],
}
);
} else {
await User.updateMany(
{
_id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
}
},
{
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
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
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,
issuer,
cert
} = req.body;
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
if (!plan.samlSSO) return res.status(400).send({ GeneralPermissionActions.Create,
message: "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration." OrgPermissionSubjects.Sso
);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (!plan.samlSSO)
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(
new Types.ObjectId(organizationId)
);
const { const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
ciphertext: encryptedEntryPoint,
iv: entryPointIV,
tag: entryPointTag
} = client.encryptSymmetric(entryPoint, key);
const { const {
ciphertext: encryptedIssuer, ciphertext: encryptedEntryPoint,
iv: issuerIV, iv: entryPointIV,
tag: issuerTag tag: entryPointTag
} = client.encryptSymmetric(issuer, key); } = client.encryptSymmetric(entryPoint, key);
const { const {
ciphertext: encryptedCert, ciphertext: encryptedIssuer,
iv: certIV, iv: issuerIV,
tag: certTag tag: issuerTag
} = client.encryptSymmetric(cert, key); } = client.encryptSymmetric(issuer, key);
const ssoConfig = await new SSOConfig({
organization: new Types.ObjectId(organizationId),
authProvider,
isActive,
encryptedEntryPoint,
entryPointIV,
entryPointTag,
encryptedIssuer,
issuerIV,
issuerTag,
encryptedCert,
certIV,
certTag
}).save();
return res.status(200).send(ssoConfig); const {
} ciphertext: encryptedCert,
iv: certIV,
tag: certTag
} = client.encryptSymmetric(cert, key);
const ssoConfig = await new SSOConfig({
organization: new Types.ObjectId(organizationId),
authProvider,
isActive,
encryptedEntryPoint,
entryPointIV,
entryPointTag,
encryptedIssuer,
issuerIV,
issuerTag,
encryptedCert,
certIV,
certTag
}).save();
return res.status(200).send(ssoConfig);
};
+78 -188
View File
@@ -1,237 +1,127 @@
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({ organizationsController.getOrganizationPlansTable
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("billingCycle").exists().isString().isIn(["monthly", "yearly"]),
validateRequest,
organizationsController.getOrganizationPlansTable
); );
router.get( router.get(
"/:organizationId/plan", "/:organizationId/plan",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationPlan
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("workspaceId").optional().isString(),
validateRequest,
organizationsController.getOrganizationPlan
); );
router.post( router.post(
"/:organizationId/session/trial", "/:organizationId/session/trial",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.startOrganizationTrial
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("success_url").exists().trim(),
validateRequest,
organizationsController.startOrganizationTrial
); );
router.get( router.get(
"/:organizationId/plan/billing", "/:organizationId/plan/billing",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationPlanBillingInfo
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("workspaceId").optional().isString(),
validateRequest,
organizationsController.getOrganizationPlanBillingInfo
); );
router.get( router.get(
"/:organizationId/plan/table", "/:organizationId/plan/table",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationPlanTable
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("workspaceId").optional().isString(),
validateRequest,
organizationsController.getOrganizationPlanTable
); );
router.get( router.get(
"/:organizationId/billing-details", "/:organizationId/billing-details",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationBillingDetails
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationBillingDetails
); );
router.patch( router.patch(
"/:organizationId/billing-details", "/:organizationId/billing-details",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.updateOrganizationBillingDetails
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("email").optional().isString().trim(),
body("name").optional().isString().trim(),
validateRequest,
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({ organizationsController.getOrganizationPmtMethods
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
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({ organizationsController.addOrganizationPmtMethod
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("success_url").exists().isString(),
body("cancel_url").exists().isString(),
validateRequest,
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({ organizationsController.deleteOrganizationPmtMethod
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
param("pmtMethodId").exists().trim(),
validateRequest,
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({ organizationsController.getOrganizationTaxIds
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
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({ organizationsController.addOrganizationTaxId
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("type").exists().isString(),
body("value").exists().isString(),
validateRequest,
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({ organizationsController.deleteOrganizationTaxId
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
param("taxId").exists().trim(),
validateRequest,
organizationsController.deleteOrganizationTaxId
); );
router.get( router.get(
"/:organizationId/invoices", "/:organizationId/invoices",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationInvoices
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationInvoices
); );
router.get( router.get(
"/:organizationId/licenses", "/:organizationId/licenses",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationLicenses
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationLicenses
); );
export default router; export default router;
+14 -42
View File
@@ -1,81 +1,53 @@
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
); );
export default router; export default router;
+59 -110
View File
@@ -1,146 +1,95 @@
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", passport.authenticate("google", {
authLimiter, scope: ["profile", "email"],
(req, res, next) => { session: false,
passport.authenticate("google", { ...(req.query.callback_port
scope: ["profile", "email"], ? {
session: false, state: req.query.callback_port as string
...(req.query.callback_port ? { }
state: req.query.callback_port as string : {})
} : {}) })(req, res, next);
})(req, res, next); });
}
);
router.get( router.get(
"/google", "/google",
passport.authenticate("google", { passport.authenticate("google", {
failureRedirect: "/login/provider/error", failureRedirect: "/login/provider/error",
session: false session: false
}), }),
ssoController.redirectSSO ssoController.redirectSSO
); );
router.get( router.get("/redirect/github", authLimiter, (req, res, next) => {
"/redirect/github", passport.authenticate("github", {
authLimiter, session: false,
(req, res, next) => { ...(req.query.callback_port
passport.authenticate("github", { ? {
session: false, state: req.query.callback_port as string
...(req.query.callback_port ? { }
state: req.query.callback_port as string : {})
} : {}) })(req, res, next);
})(req, res, next); });
}
);
router.get( router.get(
"/github", "/github",
authLimiter, authLimiter,
passport.authenticate("github", { passport.authenticate("github", {
failureRedirect: "/login/provider/error", failureRedirect: "/login/provider/error",
session: false session: false
}), }),
ssoController.redirectSSO ssoController.redirectSSO
); );
router.get( router.get("/redirect/saml2/:ssoIdentifier", authLimiter, (req, res, next) => {
"/redirect/saml2/:ssoIdentifier", const options = {
authLimiter, failureRedirect: "/",
(req, res, next) => { additionalParams: {
const options = { RelayState: req.query.callback_port ?? ""
failureRedirect: "/", }
additionalParams: { };
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(
passport.authenticate("saml", { "/saml2/:ssoIdentifier",
failureRedirect: "/login/provider/error", passport.authenticate("saml", {
failureFlash: true, failureRedirect: "/login/provider/error",
failureFlash: true,
session: false session: false
}), }),
ssoController.redirectSSO ssoController.redirectSSO
); );
router.get( router.get(
"/config", "/config",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ ssoController.getSSOConfig
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "query"
}),
query("organizationId").exists().trim(),
validateRequest,
ssoController.getSSOConfig
); );
router.post( router.post(
"/config", "/config",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ ssoController.createSSOConfig
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
); );
router.patch( router.patch(
"/config", "/config",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ ssoController.updateSSOConfig
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
); );
export default router; export default router;
+11 -14
View File
@@ -8,23 +8,20 @@ import { AuthMode } from "../../variables";
// TODO endpoint: consider moving these endpoints to be under /organization to be more RESTful // TODO endpoint: consider moving these endpoints to be under /organization to be more RESTful
router.post( router.post(
"/signup", "/signup",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
body("inviteeEmail").exists().trim().notEmpty().isEmail(), membershipOrgController.inviteUserToOrganization
body("organizationId").exists().trim().notEmpty(),
validateRequest,
membershipOrgController.inviteUserToOrganization
); );
router.post( router.post(
"/verify", "/verify",
body("email").exists().trim().notEmpty(), body("email").exists().trim().notEmpty(),
body("organizationId").exists().trim().notEmpty(), body("organizationId").exists().trim().notEmpty(),
body("code").exists().trim().notEmpty(), body("code").exists().trim().notEmpty(),
validateRequest, validateRequest,
membershipOrgController.verifyUserToOrganization membershipOrgController.verifyUserToOrganization
); );
export default router; export default router;
+37 -33
View File
@@ -9,45 +9,49 @@ 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(
"/:workspaceId/connect", // TODO endpoint: deprecate - used for old CLI (deprecate)
requireAuth({ "/:workspaceId/connect",
acceptedAuthModes: [AuthMode.JWT], requireAuth({
}), acceptedAuthModes: [AuthMode.JWT]
param("workspaceId").exists().trim(), }),
validateRequest, param("workspaceId").exists().trim(),
membershipController.validateMembership validateRequest,
membershipController.validateMembership
); );
router.delete( // TODO endpoint: check dashboard router.delete(
"/:membershipId", // TODO endpoint: check dashboard
requireAuth({ "/:membershipId",
acceptedAuthModes: [AuthMode.JWT], requireAuth({
}), acceptedAuthModes: [AuthMode.JWT]
param("membershipId").exists().trim(), }),
validateRequest, param("membershipId").exists().trim(),
membershipController.deleteMembership validateRequest,
membershipController.deleteMembership
); );
router.post( // TODO endpoint: check dashboard router.post(
"/:membershipId/change-role", // TODO endpoint: check dashboard
requireAuth({ "/:membershipId/change-role",
acceptedAuthModes: [AuthMode.JWT], requireAuth({
}), acceptedAuthModes: [AuthMode.JWT]
body("role").exists().trim(), }),
validateRequest, body("role").exists().trim(),
membershipController.changeMembershipRole validateRequest,
membershipController.changeMembershipRole
); );
router.post( // TODO endpoint: check dashboard router.post(
"/:membershipId/deny-permissions", // TODO endpoint: check dashboard
requireAuth({ "/:membershipId/deny-permissions",
acceptedAuthModes: [AuthMode.JWT], requireAuth({
}), acceptedAuthModes: [AuthMode.JWT]
param("membershipId").isMongoId().exists().trim(), }),
body("permissions").isArray().exists(), param("membershipId").isMongoId().exists().trim(),
validateRequest, body("permissions").isArray().exists(),
EEMembershipControllers.denyMembershipPermissions validateRequest,
EEMembershipControllers.denyMembershipPermissions
); );
export default router; export default router;
+14 -15
View File
@@ -5,24 +5,23 @@ 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(
"/membershipOrg/:membershipOrgId/change-role", // TODO endpoint: check dashboard
requireAuth({ "/membershipOrg/:membershipOrgId/change-role",
acceptedAuthModes: [AuthMode.JWT], requireAuth({
}), acceptedAuthModes: [AuthMode.JWT]
param("membershipOrgId"), }),
validateRequest, param("membershipOrgId"),
membershipOrgController.changeMembershipOrgRole validateRequest,
membershipOrgController.changeMembershipOrgRole
); );
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(), membershipOrgController.deleteMembershipOrg
validateRequest,
membershipOrgController.deleteMembershipOrg
); );
export default router; export default router;
+65 -132
View File
@@ -1,166 +1,99 @@
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({ "/",
acceptedAuthModes: [AuthMode.JWT], requireAuth({
}), acceptedAuthModes: [AuthMode.JWT]
organizationController.getOrganizations }),
organizationController.getOrganizations
); );
router.post( // not used on frontend router.post(
"/", // not used on frontend
requireAuth({ "/",
acceptedAuthModes: [AuthMode.JWT], requireAuth({
}), 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({ organizationController.getOrganization
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationController.getOrganization
); );
router.get( // TODO endpoint: deprecate (moved to api/v2/organizations/:organizationId/memberships) router.get(
"/:organizationId/users", // TODO endpoint: deprecate (moved to api/v2/organizations/:organizationId/memberships)
requireAuth({ "/:organizationId/users",
acceptedAuthModes: [AuthMode.JWT], requireAuth({
}), acceptedAuthModes: [AuthMode.JWT]
requireOrganizationAuth({ }),
acceptedRoles: [OWNER, ADMIN, MEMBER], organizationController.getOrganizationMembers
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationController.getOrganizationMembers
); );
router.get( // TODO endpoint: move to /v2/users/me/organizations/:organizationId/workspaces router.get(
"/:organizationId/my-workspaces", // deprecated (moved to api/v2/organizations/:organizationId/workspaces) // TODO endpoint: move to /v2/users/me/organizations/:organizationId/workspaces
requireAuth({ "/:organizationId/my-workspaces", // deprecated (moved to api/v2/organizations/:organizationId/workspaces)
acceptedAuthModes: [AuthMode.JWT], requireAuth({
}), acceptedAuthModes: [AuthMode.JWT]
requireOrganizationAuth({ }),
acceptedRoles: [OWNER, ADMIN, MEMBER], organizationController.getOrganizationWorkspaces
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationController.getOrganizationWorkspaces
); );
router.patch( router.patch(
"/:organizationId/name", "/:organizationId/name",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationController.changeOrganizationName
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("name").exists().trim().notEmpty(),
validateRequest,
organizationController.changeOrganizationName
); );
router.get( router.get(
"/:organizationId/incidentContactOrg", "/:organizationId/incidentContactOrg",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationController.getOrganizationIncidentContacts
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationController.getOrganizationIncidentContacts
); );
router.post( router.post(
"/:organizationId/incidentContactOrg", "/:organizationId/incidentContactOrg",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationController.addOrganizationIncidentContact
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("email").exists().trim().notEmpty(),
validateRequest,
organizationController.addOrganizationIncidentContact
); );
router.delete( router.delete(
"/:organizationId/incidentContactOrg", "/:organizationId/incidentContactOrg",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationController.deleteOrganizationIncidentContact
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("email").exists().trim().notEmpty(),
validateRequest,
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({ organizationController.createOrganizationPortalSession
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationController.createOrganizationPortalSession
); );
router.get( router.get(
"/:organizationId/workspace-memberships", "/:organizationId/workspace-memberships",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationController.getOrganizationMembersAndTheirWorkspaces
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationController.getOrganizationMembersAndTheirWorkspaces
); );
export default router; export default router;
+12 -51
View File
@@ -2,95 +2,56 @@ 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]
}), }),
organizationsController.getOrganizationWorkspaces
);
router.get(
// TODO endpoint: deprecate service accounts
"/:organizationId/service-accounts",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN], acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
}), }),
organizationsController.getOrganizationWorkspaces organizationsController.getOrganizationServiceAccounts
);
router.get( // TODO endpoint: deprecate service accounts
"/:organizationId/service-accounts",
param("organizationId").exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
}),
organizationsController.getOrganizationServiceAccounts
); );
export default router; export default router;
+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" });
+55 -39
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
@@ -22,35 +16,57 @@ import { ActorType } from "../ee/models";
* @param {MembershipOrg} - validated organization membership * @param {MembershipOrg} - validated organization membership
*/ */
export const validateClientForMembershipOrg = async ({ export const validateClientForMembershipOrg = async ({
authData, authData,
membershipOrgId, membershipOrgId,
acceptedRoles, acceptedRoles,
acceptedStatuses, acceptedStatuses
}: { }: {
authData: AuthData; authData: AuthData;
membershipOrgId: Types.ObjectId; membershipOrgId: Types.ObjectId;
acceptedRoles: Array<"owner" | "admin" | "member">; acceptedRoles: Array<"owner" | "admin" | "member">;
acceptedStatuses: Array<"invited" | "accepted">; acceptedStatuses: Array<"invited" | "accepted">;
}) => { }) => {
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) {
case ActorType.USER: switch (authData.actor.type) {
await validateMembershipOrg({ case ActorType.USER:
userId: authData.authPayload._id, await validateMembershipOrg({
organizationId: membershipOrg.organization, userId: authData.authPayload._id,
acceptedRoles, organizationId: membershipOrg.organization,
acceptedStatuses, acceptedRoles,
}); acceptedStatuses
});
return membershipOrg;
case ActorType.SERVICE: return membershipOrg;
throw UnauthorizedRequestError({ case ActorType.SERVICE:
message: "Failed service account client authorization for organization membership", throw UnauthorizedRequestError({
}); message: "Failed service account client authorization for organization membership"
} });
} }
};
export const DelOrgMembershipv1 = z.object({
params: z.object({
membershipOrgId: z.string().trim()
})
});
export const InviteUserToOrgv1 = z.object({
body: z.object({
inviteeEmail: z.string().trim().email(),
organizationId: z.string().trim()
})
});
export const VerifyUserToOrgv1 = z.object({
body: z.object({
email: z.string().trim().email(),
organizationId: z.string().trim(),
code: z.string().trim()
})
});
+159 -15
View File
@@ -1,12 +1,7 @@
import { Types } from "mongoose"; import { 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,10 +27,10 @@ export const validateClientForOrganization = async ({
if (!organization) { if (!organization) {
throw OrganizationNotFoundError({ throw OrganizationNotFoundError({
message: "Failed to find organization", message: "Failed to find organization"
}); });
} }
let membershipOrg; let membershipOrg;
switch (authData.actor.type) { switch (authData.actor.type) {
case ActorType.USER: case ActorType.USER:
@@ -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()
})
});
+105 -112
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
@@ -32,106 +21,110 @@ import { extractIPDetails } from "../utils/ip";
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/ */
export const validateClientForWorkspace = async ({ export const validateClientForWorkspace = async ({
authData, authData,
workspaceId, workspaceId,
environment, environment,
acceptedRoles, acceptedRoles,
requiredPermissions, requiredPermissions,
requireBlindIndicesEnabled, requireBlindIndicesEnabled,
requireE2EEOff, requireE2EEOff,
checkIPAllowlist checkIPAllowlist
}: { }: {
authData: AuthData; authData: AuthData;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment?: string; environment?: string;
acceptedRoles: Array<"admin" | "member">; acceptedRoles: Array<"admin" | "member">;
requiredPermissions?: string[]; requiredPermissions?: string[];
requireBlindIndicesEnabled: boolean; requireBlindIndicesEnabled: boolean;
requireE2EEOff: boolean; requireE2EEOff: boolean;
checkIPAllowlist: boolean; checkIPAllowlist: boolean;
}) => { }) => {
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) {
// case: blind indices are not enabled for secrets in this workspace // case: blind indices are not enabled for secrets in this workspace
// (i.e. workspace was created before blind indices were introduced // (i.e. workspace was created before blind indices were introduced
// and no admin has enabled it) // and no admin has enabled it)
const secretBlindIndexData = await SecretBlindIndexData.exists({
workspace: new Types.ObjectId(workspaceId),
});
if (!secretBlindIndexData) throw UnauthorizedRequestError({
message: "Failed workspace authorization due to blind indices not being enabled",
});
}
if (requireE2EEOff) {
const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId);
if (isWorkspaceE2EE) throw BadRequestError({
message: "Failed workspace authorization due to end-to-end encryption not being disabled",
});
}
let membership;
switch (authData.actor.type) {
case ActorType.USER:
membership = await validateUserClientForWorkspace({
user: authData.authPayload as IUser,
workspaceId,
environment,
acceptedRoles,
requiredPermissions,
});
return ({ membership, workspace });
case ActorType.SERVICE:
if (checkIPAllowlist) {
const trustedIps = await TrustedIP.find({
workspace: workspaceId
});
if (trustedIps.length > 0) {
// case: check the IP address of the inbound request against trusted IPs
const blockList = new net.BlockList(); const secretBlindIndexData = await SecretBlindIndexData.exists({
workspace: new Types.ObjectId(workspaceId)
for (const trustedIp of trustedIps) { });
if (trustedIp.prefix !== undefined) {
blockList.addSubnet(
trustedIp.ipAddress,
trustedIp.prefix,
trustedIp.type
);
} else {
blockList.addAddress(
trustedIp.ipAddress,
trustedIp.type
);
}
}
const { type } = extractIPDetails(authData.ipAddress);
const check = blockList.check(authData.ipAddress, type);
if (!check) throw UnauthorizedRequestError({
message: "Failed workspace authorization"
});
}
}
await validateServiceTokenDataClientForWorkspace({ if (!secretBlindIndexData)
serviceTokenData: authData.authPayload as IServiceTokenData, throw UnauthorizedRequestError({
workspaceId, message: "Failed workspace authorization due to blind indices not being enabled"
environment, });
requiredPermissions, }
});
if (requireE2EEOff) {
return {}; const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId);
}
} if (isWorkspaceE2EE)
throw BadRequestError({
message: "Failed workspace authorization due to end-to-end encryption not being disabled"
});
}
let membership;
switch (authData.actor.type) {
case ActorType.USER:
membership = await validateUserClientForWorkspace({
user: authData.authPayload as IUser,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
return { membership, workspace };
case ActorType.SERVICE:
if (checkIPAllowlist) {
const trustedIps = await TrustedIP.find({
workspace: workspaceId
});
if (trustedIps.length > 0) {
// case: check the IP address of the inbound request against trusted IPs
const blockList = new net.BlockList();
for (const trustedIp of trustedIps) {
if (trustedIp.prefix !== undefined) {
blockList.addSubnet(trustedIp.ipAddress, trustedIp.prefix, trustedIp.type);
} else {
blockList.addAddress(trustedIp.ipAddress, trustedIp.type);
}
}
const { type } = extractIPDetails(authData.ipAddress);
const check = blockList.check(authData.ipAddress, type);
if (!check)
throw UnauthorizedRequestError({
message: "Failed workspace authorization"
});
}
}
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload as IServiceTokenData,
workspaceId,
environment,
requiredPermissions
});
return {};
}
};
export const CreateWorkspacev1 = z.object({
body: z.object({
workspaceName: z.string().trim(),
organizationId: z.string().trim()
})
});
+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"> {isCustom &&
<Table> PERMISSIONS.map(({ action, label }) => (
<THead> <Controller
<Tr> name={`permissions.workspace.${action}`}
<Th /> key={`permissions.workspace.${action}`}
<Th className="text-center">Read</Th> control={control}
<Th className="text-center">Create</Th> render={({ field }) => (
<Th className="text-center">Edit</Th> <Checkbox
<Th className="text-center">Delete</Th> isChecked={field.value}
</Tr> onCheckedChange={field.onChange}
</THead> id={`permissions.workspace.${action}`}
<TBody> isDisabled={isNonEditable}
{isCustom && >
workspaces?.map(({ name, _id: id }) => ( {label}
<Tr key={`custom-role-ws-${name}`}> </Checkbox>
<Td>{name}</Td> )}
<Td> />
<Controller ))}
name={`permissions.workspace.${id}.read`}
control={control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.workspace.${id}.read`}
isDisabled={isNonEditable}
/>
</div>
)}
/>
</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>
); );