fix(admin): gate the dimension repair as system maintenance (#1182)
* fix(admin): gate the dimension repair as system maintenance (#1181) The endpoint's candidate query is unscoped, so it walks every event in the install, reads every original off S3 or the NAS mount, and rewrites their metadata. It required only photos.edit, which the built-in team_photographer preset holds (175_granular_permissions_and_presets.js:106) — a role that exists for a contributing second shooter, not for someone who should be able to start a whole-library scan or touch another owner's events. Now system.manage, whose own description is "run system maintenance actions", with the status endpoint on system.view to match. Nobody who should have it loses it: super_admin is granted every permission, solo_photographer is 'ALL', and migration 175 already projects every settings.edit holder forward onto system.manage on upgrade. The capture-date sweep next to it was gated this way in #1179; this brings its older twin in line. * fix(admin): gate the dimension status card on the permission the button needs (#1181) Same mismatch as the capture-date card: system.view and system.manage are independent grants and StatusTab renders its card and enabled button purely on a successful status payload (StatusTab.tsx:558), so a system.view-only role got a live Repair button whose every click 403s. * fix(admin): stop the dimension status card polling a 403 (#1181) With the endpoint correctly requiring system.manage, anyone who can open the Status tab but lacks it would have had a 403 and a logged denial every ten seconds for a panel they were never shown. The query is now gated on the same permission the endpoint requires, so it never starts. * fix(admin): gate the dimension card's render on the permission too (#1181) TanStack keeps the cached status after `enabled` flips false, so checking only the payload would still show the card — and an enabled Repair button whose POST 403s — to a lower-privileged admin logging in behind a system.manage user inside the cache lifetime. * fix(admin): name the dimension-card permission flag for the card it gates (#1181) #1179 adds a second system.manage-gated card to this same component with the same flag name. Two identical declarations merge WITHOUT a conflict and then fail to compile — TS2451, cannot redeclare block-scoped variable — and since each PR is green on its own, nothing catches it until main's build breaks. Verified by trial-merging both into main: no conflict, two declarations, tsc fails on both lines. Naming this one for the card it gates removes the trap; once both have landed the two flags can collapse into one. --------- Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
co-authored by
Paul Nothaft
parent
51d20c5920
commit
3991dc3ccb
@@ -64,7 +64,14 @@ function startLeaseKeeper(jobName, token) {
|
||||
}
|
||||
|
||||
// Repair photo dimensions (background job)
|
||||
router.post('/repair-dimensions', adminAuth, requirePermission('photos.edit'), async (req, res) => {
|
||||
//
|
||||
// system.manage, not photos.edit: the query below is unscoped, so this walks
|
||||
// every event in the install, reads every original off S3 or the NAS mount, and
|
||||
// rewrites their metadata. photos.edit is held by the team_photographer preset
|
||||
// (175_granular_permissions_and_presets.js:106), which exists for a contributing
|
||||
// second shooter — someone who should be able to edit the photos they work on,
|
||||
// not start a whole-library scan or touch another owner's events.
|
||||
router.post('/repair-dimensions', adminAuth, requirePermission('system.manage'), async (req, res) => {
|
||||
try {
|
||||
// Claimed before the candidate query, not after: that query is an await,
|
||||
// and two requests arriving inside it would both read "not running" and
|
||||
@@ -211,7 +218,13 @@ router.post('/repair-dimensions', adminAuth, requirePermission('photos.edit'), a
|
||||
});
|
||||
|
||||
// Get dimension repair status
|
||||
router.get('/repair-dimensions/status', adminAuth, requirePermission('photos.view'), async (req, res) => {
|
||||
//
|
||||
// The same permission as the POST, not the read-only system.view. The two are
|
||||
// independent grants, and StatusTab has no permission gate of its own — a
|
||||
// successful status payload is what renders the card and its enabled button
|
||||
// (StatusTab.tsx:558). system.view alone would therefore show a live Repair
|
||||
// button whose every click 403s with no error surfaced.
|
||||
router.get('/repair-dimensions/status', adminAuth, requirePermission('system.manage'), async (req, res) => {
|
||||
try {
|
||||
const totalPhotos = await db('photos')
|
||||
.where(function () {
|
||||
|
||||
@@ -62,13 +62,25 @@ export const StatusTab: React.FC<StatusTabProps> = ({
|
||||
const { storageInfo, systemStatus } = useStatusTab(isActive);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Gated on the same permission the endpoint requires, so a role without it
|
||||
// never starts the poll. Without this the card would poll a 403 every ten
|
||||
// seconds for anyone who can open the Status tab but cannot run the repair,
|
||||
// filling the logs with denials for a panel they were never shown.
|
||||
//
|
||||
// Named for the card it gates rather than for the permission, because #1179
|
||||
// adds a second system.manage-gated card to this same component. Two flags
|
||||
// with one name merge without a conflict and then fail to compile
|
||||
// (TS2451) — and since each PR is green on its own, nothing catches it until
|
||||
// main's build breaks. Once both have landed these can collapse into one.
|
||||
const canRepairDimensions = usePermission('system.manage');
|
||||
|
||||
const { data: dimensionStatus } = useQuery({
|
||||
queryKey: ['photo-dimension-status'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/admin/photos/repair-dimensions/status');
|
||||
return res.data;
|
||||
},
|
||||
enabled: isActive,
|
||||
enabled: isActive && canRepairDimensions,
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
|
||||
@@ -594,7 +606,11 @@ export const StatusTab: React.FC<StatusTabProps> = ({
|
||||
)}
|
||||
|
||||
{/* Photo Dimensions */}
|
||||
{dimensionStatus && (
|
||||
{/* canRepairDimensions as well as the payload: TanStack keeps the cached
|
||||
status after `enabled` flips false, so without it a lower-privileged
|
||||
admin logging in behind a system.manage user inside the cache lifetime
|
||||
would still be shown the card and a button whose POST 403s. */}
|
||||
{dimensionStatus && canRepairDimensions && (
|
||||
<Card padding="md">
|
||||
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4 flex items-center gap-2">
|
||||
<Ruler className="w-5 h-5" />
|
||||
|
||||
Reference in New Issue
Block a user