Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/backend/src/controllers/rules.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export default class RulesController {

static async getAllRulesetTypes(req: Request, res: Response, next: NextFunction) {
try {
const rulesets = await RulesService.getAllRulesetTypes(req.organization, req.currentCar?.carId);
const rulesets = await RulesService.getAllRulesetTypes(req.currentUser, req.organization, req.currentCar?.carId);
res.status(200).json(rulesets);
} catch (error: unknown) {
next(error);
Expand All @@ -135,6 +135,7 @@ export default class RulesController {
try {
const { rulesetTypeId } = req.params as Record<string, string>;
const rulesets = await RulesService.getRulesetsByRulesetType(
req.currentUser,
rulesetTypeId,
req.organization.organizationId,
req.currentCar?.carId
Expand All @@ -149,6 +150,7 @@ export default class RulesController {
try {
const { rulesetTypeId } = req.params as Record<string, string>;
const rulesetType = await RulesService.getRulesetType(
req.currentUser,
rulesetTypeId,
req.organization.organizationId,
req.currentCar?.carId
Expand Down Expand Up @@ -315,7 +317,7 @@ export default class RulesController {
static async getChildRules(req: Request, res: Response, next: NextFunction) {
try {
const { ruleId: parentRuleId } = req.params as Record<string, string>;
const childrenRules: Rule[] = await RulesService.getChildRules(parentRuleId, req.organization);
const childrenRules: Rule[] = await RulesService.getChildRules(req.currentUser, parentRuleId, req.organization);

res.status(200).json(childrenRules);
} catch (error: unknown) {
Expand All @@ -328,6 +330,7 @@ export default class RulesController {
const { rulesetId, projectId } = req.params as Record<string, string>;

const rules = await RulesService.getUnassignedRulesForProjectRuleset(
req.currentUser,
rulesetId,
projectId,
req.organization.organizationId
Expand All @@ -342,7 +345,7 @@ export default class RulesController {
try {
const { rulesetId, projectId } = req.params as Record<string, string>;

const projectRules = await RulesService.getProjectRules(rulesetId, projectId, req.organization);
const projectRules = await RulesService.getProjectRules(req.currentUser, rulesetId, projectId, req.organization);

res.status(200).json(projectRules);
} catch (error: unknown) {
Expand All @@ -353,7 +356,7 @@ export default class RulesController {
static async getTopLevelRules(req: Request, res: Response, next: NextFunction) {
try {
const { rulesetId } = req.params as Record<string, string>;
const rules = await RulesService.getTopLevelRules(rulesetId, req.organization.organizationId);
const rules = await RulesService.getTopLevelRules(req.currentUser, rulesetId, req.organization.organizationId);
res.status(200).json(rules);
} catch (error: unknown) {
next(error);
Expand All @@ -363,7 +366,7 @@ export default class RulesController {
static async getAllRulesForRuleset(req: Request, res: Response, next: NextFunction) {
try {
const { rulesetId } = req.params as Record<string, string>;
const rules = await RulesService.getAllRulesForRuleset(rulesetId, req.organization.organizationId);
const rules = await RulesService.getAllRulesForRuleset(req.currentUser, rulesetId, req.organization.organizationId);
res.status(200).json(rules);
} catch (error: unknown) {
next(error);
Expand Down
121 changes: 95 additions & 26 deletions src/backend/src/services/rules.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ import {
NotFoundException
} from '../utils/errors.utils.js';
import { userHasPermission } from '../utils/users.utils.js';
import { isUserPartOfTeams } from '../utils/teams.utils.js';
import {
getProjectRuleQueryArgs,
getRulesetQueryArgs,
getRulePreviewQueryArgs,
getRulesetTypeQueryArgs,
getRuleStatusHistoryQueryArgs
} from '../prisma-query-args/rules.query-args.js';
import { getTeamPreviewQueryArgs } from '../prisma-query-args/teams.query-args.js';
import {
ruleTransformer,
projectRuleTransformer,
Expand Down Expand Up @@ -220,9 +222,9 @@ export default class RulesService {
referencedRuleIds: string[] = [],
imageFileIds: string[] = []
) {
// Check user has permission (members and above)
if (!(await userHasPermission(user.userId, organization.organizationId, notGuest))) {
throw new AccessDeniedException('Only members and above can create rules');
// Check user has permission (leadership and above)
if (!(await userHasPermission(user.userId, organization.organizationId, isLeadership))) {
throw new AccessDeniedException('Only leadership and above can create rules');
}

// Verify ruleset exists and belongs to organization
Expand Down Expand Up @@ -804,10 +806,15 @@ export default class RulesService {
static async deleteRuleset(rulesetId: string, deleterId: string, organizationId: string) {
const ruleset = await RulesService.getRulesetWithQueryArgs(rulesetId);

// admins can delete any ruleset; leadership and heads can only delete a ruleset they created themselves
const isCreator = deleterId === ruleset.createdByUserId;
const hasPermission =
(await userHasPermission(deleterId, organizationId, isAdmin)) || deleterId === ruleset.createdByUserId;
(await userHasPermission(deleterId, organizationId, isAdmin)) ||
(isCreator && (await userHasPermission(deleterId, organizationId, isLeadership)));

if (!hasPermission) throw new AccessDeniedException('Only admins can delete a ruleset.');
if (!hasPermission) {
throw new AccessDeniedException('You do not have permissions to delete this ruleset.');
}

if (ruleset.active) {
throw new HttpException(400, 'Cannot delete an active ruleset. Please deactivate it first.');
Expand All @@ -822,7 +829,11 @@ export default class RulesService {
return rulesetTransformer(deletedRuleset);
}

static async getAllRulesetTypes(organization: Organization, carId?: string): Promise<RulesetType[]> {
static async getAllRulesetTypes(user: User, organization: Organization, carId?: string): Promise<RulesetType[]> {
if (!(await userHasPermission(user.userId, organization.organizationId, notGuest))) {
throw new AccessDeniedGuestException('view ruleset types');
}

const rulesets = await prisma.ruleset_Type.findMany({
where: {
organizationId: organization.organizationId,
Expand All @@ -835,12 +846,22 @@ export default class RulesService {

/**
* Gets a ruleset type for a given ruleset type ID
* @param user the user requesting the ruleset type
* @param rulesetTypeId id of ruleset type
* @param organizationId id of organization
* @param carId optional id of the car to scope revision file counts to
* @returns ruleset type associated with provided ruleset type ID
*/
static async getRulesetType(rulesetTypeId: string, organizationId: string, carId?: string): Promise<RulesetType> {
static async getRulesetType(
user: User,
rulesetTypeId: string,
organizationId: string,
carId?: string
): Promise<RulesetType> {
if (!(await userHasPermission(user.userId, organizationId, notGuest))) {
throw new AccessDeniedGuestException('view ruleset types');
}

const rulesetType = await prisma.ruleset_Type.findUnique({
where: {
rulesetTypeId,
Expand All @@ -863,12 +884,22 @@ export default class RulesService {

/**
* Gets rulesets for a given ruleset type
* @param user the user requesting the rulesets
* @param rulesetTypeId id of ruleset type
* @param organizationId id of organization
* @param carId optional id of the car to filter rulesets by
* @returns rulesets associated with provided ruleset type
*/
static async getRulesetsByRulesetType(rulesetTypeId: string, organizationId: string, carId?: string): Promise<Ruleset[]> {
static async getRulesetsByRulesetType(
user: User,
rulesetTypeId: string,
organizationId: string,
carId?: string
): Promise<Ruleset[]> {
if (!(await userHasPermission(user.userId, organizationId, notGuest))) {
throw new AccessDeniedGuestException('view rulesets');
}

const rulesets = await prisma.ruleset.findMany({
where: {
rulesetTypeId,
Expand All @@ -890,6 +921,7 @@ export default class RulesService {
/**
* Sets a rule's general-view status. This status is independent of any project.
* It is unaffected by the status of the rule in any project it's assigned to.
* Only leadership and above can update the general-view status.
* @param submitter the user updating the status
* @param organization the organization of the rule
* @param ruleId the id of the rule to update
Expand Down Expand Up @@ -956,6 +988,7 @@ export default class RulesService {
/**
* Sets a rule's status within a single project. This status is local to that project.
* It does not affect the rule's general-view status, or its status in any other project.
* Leadership and above can update it anywhere; members can only update it for a project whose team they're on.
* @param submitter the user updating the status
* @param organization the organization of the project rule
* @param projectRuleId the id of the project rule to update
Expand All @@ -968,14 +1001,10 @@ export default class RulesService {
projectRuleId: string,
status: RuleStatus
): Promise<ProjectRule> {
if (!(await userHasPermission(submitter.userId, organization.organizationId, isLeadership))) {
throw new AccessDeniedException('You do not have permissions to update rule status');
}

const projectRule = await prisma.project_Rule.findUnique({
where: { projectRuleId },
include: {
project: { include: { wbsElement: true } },
project: { include: { wbsElement: true, teams: getTeamPreviewQueryArgs(organization.organizationId) } },
rule: { include: { ruleset: { include: { car: { include: { wbsElement: true } } } } } }
}
});
Expand All @@ -995,6 +1024,11 @@ export default class RulesService {
throw new InvalidOrganizationException('Project Rule');
}

const hasOrgWidePermission = await userHasPermission(submitter.userId, organization.organizationId, isLeadership);
if (!hasOrgWidePermission && !isUserPartOfTeams(projectRule.project.teams, submitter)) {
throw new AccessDeniedException('You do not have permissions to update rule status for this project');
}

const childProjectRuleCount = await prisma.project_Rule.count({
where: { projectId: projectRule.projectId, dateDeleted: null, rule: { parentRuleId: projectRule.ruleId } }
});
Expand Down Expand Up @@ -1028,15 +1062,15 @@ export default class RulesService {
/**
* Resets every rule's general-view status back to Pending, for a whole ruleset.
* Does not affect any rule's status within a project. Never creates history entries,
* since reverting to PENDING is not tracked.
* since reverting to PENDING is not tracked. Only admins and above can do this.
* @param submitter the user resetting the statuses
* @param organization the organization of the ruleset
* @param rulesetId the id of the ruleset to reset
* @returns the number of rules that were reset
*/
static async resetRulesetStatuses(submitter: User, organization: Organization, rulesetId: string): Promise<number> {
if (!(await userHasPermission(submitter.userId, organization.organizationId, isLeadership))) {
throw new AccessDeniedException('You do not have permissions to update rule status');
if (!(await userHasPermission(submitter.userId, organization.organizationId, isAdmin))) {
throw new AccessDeniedException('You do not have permissions to reset rule status');
}

const ruleset = await prisma.ruleset.findUnique({
Expand Down Expand Up @@ -1068,7 +1102,7 @@ export default class RulesService {
* Resets every project rule's status back to Pending, for a single project, scoped to a
* single ruleset (a project can have rules from multiple ruleset types). Does not affect
* any rule's general-view status, or its status in any other project. Never creates history
* entries, since reverting to PENDING is not tracked.
* entries, since reverting to PENDING is not tracked. Allowed for leadership and up.
* @param submitter the user resetting the statuses
* @param organization the organization of the project and ruleset
* @param rulesetId the ruleset to scope the reset to
Expand All @@ -1082,7 +1116,7 @@ export default class RulesService {
projectId: string
): Promise<number> {
if (!(await userHasPermission(submitter.userId, organization.organizationId, isLeadership))) {
throw new AccessDeniedException('You do not have permissions to update rule status');
throw new AccessDeniedException('You do not have permissions to reset project rule status');
}

const ruleset = await prisma.ruleset.findUnique({
Expand Down Expand Up @@ -1182,9 +1216,9 @@ export default class RulesService {
*
*/
static async toggleRuleTeam(ruleId: string, teamId: string, user: User, org: Organization) {
// Checks that the user is not a guest
if (!(await userHasPermission(user.userId, org.organizationId, notGuest))) {
throw new AccessDeniedGuestException('Toggle Rule Team');
// Checks that the user is leadership and above
if (!(await userHasPermission(user.userId, org.organizationId, isLeadership))) {
throw new AccessDeniedException('Only leadership and above can assign rules to teams');
}

// Checks that the rule exists and is not deleted
Expand Down Expand Up @@ -1513,11 +1547,16 @@ export default class RulesService {

/**
* Gets all subrules of a specific rule.
* @param user the user requesting the child rules
* @param ruleId the ID of the parent rule
* @param organization the organization the rule belongs to
* @returns an array of all child rules (the Rule object)
*/
static async getChildRules(ruleId: string, organization: Organization): Promise<SharedRule[]> {
static async getChildRules(user: User, ruleId: string, organization: Organization): Promise<SharedRule[]> {
if (!(await userHasPermission(user.userId, organization.organizationId, notGuest))) {
throw new AccessDeniedGuestException('view rules');
}

// Verify the parent rule exists and belongs to the organization
const parentRule = await prisma.rule.findUnique({
where: { ruleId },
Expand Down Expand Up @@ -1561,12 +1600,22 @@ export default class RulesService {
/**
* Gets rules assignable to a project that are not already assigned to it.
* A project can belong to multiple teams, so rules from all of its teams are shown.
* @param user the user requesting the unassigned rules
* @param rulesetId ruleset the rules are in
* @param projectId the project the rules would be assigned to
* @param organizationId the organization id
* @returns the rules on one of the project's teams that are not already actively assigned to this project
*/
static async getUnassignedRulesForProjectRuleset(rulesetId: string, projectId: string, organizationId: string) {
static async getUnassignedRulesForProjectRuleset(
user: User,
rulesetId: string,
projectId: string,
organizationId: string
) {
if (!(await userHasPermission(user.userId, organizationId, notGuest))) {
throw new AccessDeniedGuestException('view unassigned rules');
}

const ruleset = await prisma.ruleset.findUnique({
where: { rulesetId },
select: {
Expand Down Expand Up @@ -1635,12 +1684,22 @@ export default class RulesService {

/**
* Gets all rules associated with a specific project and ruleset
* @param user the user requesting the project rules
* @param rulesetId the id of the ruleset
* @param projectId the id of the project
* @param organization the organization the project and ruleset belong to
* @returns Array of ProjectRule objects
*/
static async getProjectRules(rulesetId: string, projectId: string, organization: Organization): Promise<ProjectRule[]> {
static async getProjectRules(
user: User,
rulesetId: string,
projectId: string,
organization: Organization
): Promise<ProjectRule[]> {
if (!(await userHasPermission(user.userId, organization.organizationId, notGuest))) {
throw new AccessDeniedGuestException('view project rules');
}

const ruleset = await prisma.ruleset.findUnique({
where: { rulesetId },
include: {
Expand Down Expand Up @@ -1701,10 +1760,15 @@ export default class RulesService {

/**
* Gets all rules with no parent id
* @param user the user requesting the top-level rules
* @param rulesetId id of ruleset
* @returns an array of rules with no parent Id
*/
static async getTopLevelRules(rulesetId: string, organizationId: string) {
static async getTopLevelRules(user: User, rulesetId: string, organizationId: string) {
if (!(await userHasPermission(user.userId, organizationId, notGuest))) {
throw new AccessDeniedGuestException('view rules');
}

const ruleset = await prisma.ruleset.findUnique({
where: { rulesetId },
select: {
Expand Down Expand Up @@ -1744,11 +1808,16 @@ export default class RulesService {

/**
* Gets every rule in a ruleset in a single query instead of walking it level by level.
* @param user the user requesting the rules
* @param rulesetId id of ruleset
* @param organizationId the organization the ruleset belongs to
* @returns a flat array of every rule in the ruleset
*/
static async getAllRulesForRuleset(rulesetId: string, organizationId: string): Promise<SharedRule[]> {
static async getAllRulesForRuleset(user: User, rulesetId: string, organizationId: string): Promise<SharedRule[]> {
if (!(await userHasPermission(user.userId, organizationId, notGuest))) {
throw new AccessDeniedGuestException('view rules');
}

const ruleset = await prisma.ruleset.findUnique({
where: { rulesetId },
select: {
Expand Down
Loading
Loading