feat(rbac): added new permission check for workspace in frontend

This commit is contained in:
Akhil Mohan
2023-09-08 21:22:36 +05:30
parent 549121f44e
commit 6671699867
48 changed files with 3446 additions and 2804 deletions
@@ -4,7 +4,7 @@ import { IUser, Key, Membership, MembershipOrg, User } from "../../models";
import { EventType } from "../../ee/models";
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
import { sendMail } from "../../helpers/nodemailer";
import { ACCEPTED, ADMIN, MEMBER } from "../../variables";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
import { getSiteURL } from "../../config";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
@@ -15,6 +15,8 @@ import {
getUserProjectPermissions
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import Role from "../../models/role";
import { BadRequestError } from "../../utils/errors";
/**
* Check that user is a member of workspace with id [workspaceId]
@@ -107,10 +109,6 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
params: { membershipId }
} = await validateRequest(reqValidator.ChangeMembershipRoleV1, req);
if (![ADMIN, MEMBER].includes(role)) {
throw new Error("Failed to validate role");
}
// validate target membership
const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>(
"user"
@@ -129,9 +127,32 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
ProjectPermissionSub.Member
);
const oldRole = membershipToChangeRole.role;
membershipToChangeRole.role = role;
await membershipToChangeRole.save();
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
if (isCustomRole) {
const wsRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: membershipToChangeRole.workspace
});
if (!wsRole) throw BadRequestError({ message: "Role not found" });
const membership = await Membership.findByIdAndUpdate(membershipId, {
role: CUSTOM,
customRole: wsRole
});
return res.status(200).send({
membership
});
}
const membership = await Membership.findByIdAndUpdate(
membershipId,
{
role
},
{
new: true
}
);
await EEAuditLogService.createAuditLog(
req.authData,
@@ -140,8 +161,8 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
metadata: {
userId: membershipToChangeRole.user._id.toString(),
email: membershipToChangeRole.user.email,
oldRole,
newRole: membershipToChangeRole.role
oldRole: membershipToChangeRole.role,
newRole: role
}
},
{
@@ -150,7 +171,7 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
);
return res.status(200).send({
membership: membershipToChangeRole
membership
});
};
@@ -25,7 +25,6 @@ import {
userHasWriteOnlyAbility
} from "../../ee/helpers/checkMembershipPermissions";
import _ from "lodash";
import { BatchSecret } from "../../types/secret";
import {
getFolderByPath,
getFolderIdFromServiceToken,
@@ -56,8 +55,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
body: { secretPath, folderId }
} = validatedData;
const createSecrets: BatchSecret[] = [];
const updateSecrets: BatchSecret[] = [];
const createSecrets: any[] = [];
const updateSecrets: any[] = [];
const deleteSecrets: { _id: Types.ObjectId; secretName: string }[] = [];
const actions: IAction[] = [];
@@ -111,7 +110,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
version: 1,
user: request.secret.type === SECRET_PERSONAL ? req.user : undefined,
environment,
workspace: new Types.ObjectId(workspaceId),
workspace: workspaceId,
folder: folderId,
secretBlindIndex,
algorithm: ALGORITHM_AES_256_GCM,
@@ -126,7 +125,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
updateSecrets.push({
...request.secret,
_id: new Types.ObjectId(request.secret._id),
_id: request.secret._id,
secretBlindIndex,
folder: folderId,
algorithm: ALGORITHM_AES_256_GCM,
@@ -145,7 +144,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
// handle create secrets
let createdSecrets: ISecret[] = [];
if (createSecrets.length > 0) {
createdSecrets = await Secret.insertMany(createSecrets);
createdSecrets = (await Secret.insertMany(createSecrets)) as any;
// (EE) add secret versions for new secrets
await EESecretService.addSecretVersions({
secretVersions: createdSecrets.map((n: any) => {
+68 -67
View File
@@ -1,7 +1,4 @@
import {
IUser,
User,
} from "../models";
import { IUser, User } from "../models";
import { sendMail } from "./nodemailer";
/**
@@ -12,10 +9,10 @@ import { sendMail } from "./nodemailer";
*/
export const setupAccount = async ({ email }: { email: string }) => {
const user = await new User({
email,
email
}).save();
return user;
return user;
};
/**
@@ -37,36 +34,36 @@ export const setupAccount = async ({ email }: { email: string }) => {
* @returns {Object} user - the completed user
*/
export const completeAccount = async ({
userId,
firstName,
lastName,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
userId,
firstName,
lastName,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
}: {
userId: string;
firstName: string;
lastName: string;
encryptionVersion: number;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
userId: string;
firstName: string;
lastName?: string;
encryptionVersion: number;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
}) => {
const options = {
new: true,
new: true
};
const user = await User.findByIdAndUpdate(
userId,
@@ -82,12 +79,12 @@ export const completeAccount = async ({
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
verifier
},
options
);
return user;
return user;
};
/**
@@ -98,38 +95,42 @@ export const completeAccount = async ({
* @param {String} obj.userAgent - login user-agent
*/
export const checkUserDevice = async ({
user,
ip,
userAgent,
user,
ip,
userAgent
}: {
user: IUser;
ip: string;
userAgent: string;
user: IUser;
ip: string;
userAgent: string;
}) => {
const isDeviceSeen = user.devices.some((device) => device.ip === ip && device.userAgent === userAgent);
if (!isDeviceSeen) {
// case: unseen login ip detected for user
// -> notify user about the sign-in from new ip
user.devices = user.devices.concat([{
ip: String(ip),
userAgent,
}]);
await user.save();
const isDeviceSeen = user.devices.some(
(device) => device.ip === ip && device.userAgent === userAgent
);
// send MFA code [code] to [email]
await sendMail({
template: "newDevice.handlebars",
subjectLine: "Successful login from new device",
recipients: [user.email],
substitutions: {
email: user.email,
timestamp: new Date().toString(),
ip,
userAgent,
},
});
}
}
if (!isDeviceSeen) {
// case: unseen login ip detected for user
// -> notify user about the sign-in from new ip
user.devices = user.devices.concat([
{
ip: String(ip),
userAgent
}
]);
await user.save();
// send MFA code [code] to [email]
await sendMail({
template: "newDevice.handlebars",
subjectLine: "Successful login from new device",
recipients: [user.email],
substitutions: {
email: user.email,
timestamp: new Date().toString(),
ip,
userAgent
}
});
}
};
+1 -8
View File
@@ -1,7 +1,6 @@
import express from "express";
const router = express.Router();
import { body, param } from "express-validator";
import { requireAuth, validateRequest } from "../../middleware";
import { requireAuth } from "../../middleware";
import { membershipController } from "../../controllers/v1";
import { AuthMode } from "../../variables";
@@ -14,8 +13,6 @@ router.get(
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
param("workspaceId").exists().trim(),
validateRequest,
membershipController.validateMembership
);
@@ -25,8 +22,6 @@ router.delete(
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
param("membershipId").exists().trim(),
validateRequest,
membershipController.deleteMembership
);
@@ -36,8 +31,6 @@ router.post(
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
body("role").exists().trim(),
validateRequest,
membershipController.changeMembershipRole
);
+4 -4
View File
@@ -34,7 +34,7 @@ export enum ProjectPermissionSub {
Folders = "folders"
}
type GenericFields = {
type SubjectFields = {
environment: string;
secretPath?: string;
};
@@ -42,17 +42,17 @@ type GenericFields = {
export type ProjectPermissionSet =
| [
ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & GenericFields)
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
]
| [
ProjectPermissionActions,
ProjectPermissionSub.Folders | (ForcedSubject<ProjectPermissionSub.Folders> & GenericFields)
ProjectPermissionSub.Folders | (ForcedSubject<ProjectPermissionSub.Folders> & SubjectFields)
]
| [
ProjectPermissionActions,
(
| ProjectPermissionSub.SecretImports
| (ForcedSubject<ProjectPermissionSub.SecretImports> & GenericFields)
| (ForcedSubject<ProjectPermissionSub.SecretImports> & SubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.Role]
+8 -2
View File
@@ -28,7 +28,13 @@ export interface BatchSecretRequest {
}
export interface BatchSecret {
_id: string;
version?: number;
_id?: string;
user?: string;
environment: string;
workspace?: string;
algorithm?: string;
keyEncoding?: string;
type: "shared" | "personal";
secretName: string;
secretBlindIndex: string;
@@ -42,5 +48,5 @@ export interface BatchSecret {
secretCommentIV: string;
secretCommentTag: string;
tags: string[];
folder: string
folder: string;
}
+1 -1
View File
@@ -117,7 +117,7 @@ export const CompletedAccountSignupV3 = z.object({
body: z.object({
email: z.string().email().trim(),
firstName: z.string().trim(),
lastName: z.string().trim().optional().nullish(),
lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
+10 -8
View File
@@ -115,8 +115,8 @@ export const GetSecretVersionsV1 = z.object({
secretId: z.string().trim()
}),
query: z.object({
offset: z.number(),
limit: z.number()
offset: z.coerce.number(),
limit: z.coerce.number()
})
});
@@ -175,17 +175,19 @@ const batchUpdateRequestV2 = z.object({
secretCommentCiphertext: z.string().trim().optional(),
secretCommentIV: z.string().trim().optional(),
secretCommentTag: z.string().trim().optional(),
tags: z.object({
_id: z.string().trim(),
name: z.string().trim(),
slug: z.string().trim()
})
tags: z
.object({
_id: z.string().trim(),
name: z.string().trim(),
slug: z.string().trim()
})
.array()
});
export const BatchSecretsV2 = z.object({
body: z.object({
workspaceId: z.string().trim(),
folderId: z.string().trim(),
folderId: z.string().trim().default("root"),
environment: z.string().trim(),
secretPath: z.string().trim().optional(),
requests: z
+6 -6
View File
@@ -130,8 +130,8 @@ export const GetWorkspaceSecretSnapshotsV1 = z.object({
query: z.object({
environment: z.string().trim(),
folderId: z.string().trim().default("root"),
offset: z.number(),
limit: z.number()
offset: z.coerce.number(),
limit: z.coerce.number()
})
});
@@ -161,8 +161,8 @@ export const GetWorkspaceLogsV1 = z.object({
workspaceId: z.string().trim()
}),
query: z.object({
offset: z.number(),
limit: z.number(),
offset: z.coerce.number(),
limit: z.coerce.number(),
sortBy: z.string().trim().optional(),
userId: z.string().trim().optional(),
actionNames: z.string().trim().optional()
@@ -178,8 +178,8 @@ export const GetWorkspaceAuditLogsV1 = z.object({
userAgentType: z.nativeEnum(UserAgentType).nullable().optional(),
startDate: z.string().datetime().nullable().optional(),
endDate: z.string().datetime().nullable().optional(),
offset: z.number(),
limit: z.number(),
offset: z.coerce.number(),
limit: z.coerce.number(),
actor: z.string().nullish().optional()
})
});