Files
wekan/client/components/rules/rulesList.js
T
Lauri Ojansivu 68dd14140f Fix: deleting a board rule no longer fails with "Access denied [403]"
Deleting a rule ran three separate client-side Collection.remove() calls
(Rules + Triggers + Actions). Each is gated by a per-collection allow() rule
that resolves the board from that document own boardId via
Boards.findOneAsync(doc.boardId). When a trigger/action document has no
resolvable boardId (legacy rules created before boardId was denormalised, or
docs not published to the client), the board resolves to null and
allowIsBoardAdmin returns false, so Meteor rejects the mutation with 403
"Access denied" and the delete fails even for a legitimate board admin.

Route rule deletion through a new server Meteor method rules.deleteRule, which
authorizes ONCE (active board admin or site admin) and removes the rule, its
trigger and its action server-side (bypassing the brittle client allow/deny).
The permission decision is extracted into the Meteor-free helper
models/lib/ruleDeletePermission.js and covered by
tests/ruleDeletePermission.test.cjs. No permission is loosened: only board
admins and site admins may delete, matching the board-admin gate used to
create a rule.
2026-07-09 02:14:07 +03:00

91 lines
2.6 KiB
JavaScript

import { ReactiveCache } from '/imports/reactiveCache';
import Rules from '/models/rules';
function boardRuleIds() {
const boardId = Session.get('currentBoard');
return ReactiveCache.getRules({ boardId }).map(r => r._id);
}
function getSelected() {
return Session.get('selectedRuleIds') || [];
}
function setSelected(ids) {
Session.set('selectedRuleIds', ids);
}
Template.rulesList.onCreated(function () {
this.autorun(() => {
const boardId = Session.get('currentBoard');
if (boardId) this.subscribe('boardRules', boardId);
});
this.editingRuleId = new ReactiveVar(null);
});
Template.rulesList.helpers({
rules() {
const boardId = Session.get('currentBoard');
return ReactiveCache.getRules({ boardId });
},
canAdmin() {
const user = ReactiveCache.getCurrentUser();
return user && (user.isAdmin || user.isBoardAdmin);
},
isSelected() {
return getSelected().includes(this._id);
},
isEditing() {
return Template.instance().editingRuleId.get() === this._id;
},
});
Template.rulesList.events({
'change .js-rule-select'(event) {
const ruleId = event.currentTarget.getAttribute('data-rule-id');
const selected = new Set(getSelected());
if (event.currentTarget.checked) {
selected.add(ruleId);
} else {
selected.delete(ruleId);
}
setSelected([...selected]);
},
'click .js-rules-select-all'() {
setSelected(boardRuleIds());
},
'click .js-rules-select-none'() {
setSelected([]);
},
'click .js-rules-delete-selected'() {
getSelected().forEach(ruleId => {
const rule = ReactiveCache.getRule(ruleId);
if (rule) {
// Delete the rule + its trigger + action server-side (see
// server/rulesButton.js `rules.deleteRule`): three client-side
// Collection.remove() calls were rejected with 403 "Access denied".
Meteor.call('rules.deleteRule', rule._id);
}
});
setSelected([]);
},
'click .js-rules-export-selected': Popup.open('rulesImportExport'),
// Inline rename of a rule.
'click .js-edit-rule'(event, tpl) {
tpl.editingRuleId.set(this._id);
},
'keydown .js-edit-rule-input'(event, tpl) {
if (event.key === 'Enter') {
const title = event.currentTarget.value.trim();
if (title) Rules.update(this._id, { $set: { title } });
tpl.editingRuleId.set(null);
} else if (event.key === 'Escape') {
tpl.editingRuleId.set(null);
}
},
'blur .js-edit-rule-input'(event, tpl) {
const title = event.currentTarget.value.trim();
if (title) Rules.update(this._id, { $set: { title } });
tpl.editingRuleId.set(null);
},
});