Commit Graph

4 Commits

Author SHA1 Message Date
Anso 209c9c5d53 feat: add target-aware RBAC authorization to Scheduled Operations (#1745)
* feat: add target-aware RBAC authorization to Scheduled Operations

Replace the blanket requireAdmin gate on all 9 scheduled-tasks endpoints
with per-action permission checks derived from the centralized action
registry. Each scheduled action now declares the existing permission it
requires: stack lifecycle actions need stack:deploy on the target stack,
node-wide operations need node:manage on the target node, prune stays
admin-only via system:settings, and snapshot requires unscoped
node:manage.

Key changes:
- Backend registry: add permission field and resolveTaskPermissionScope
- Routes: replace requireAdmin with requireTaskPermission, filter GET
  listing by permission, add two-phase PUT check
- Scheduler: revalidate creator permission at execution time, auto-disable
  on revocation (TaskAuthorizationError)
- Database: new creator_user_id column with migration and backfill
- Frontend: canScheduleAction/canScheduleAny helpers, reachability gate
  via scheduledOpsAccessible, stack context menu uses canDeploy
  not isAdmin, permissions field on all ScheduledActionDefinitions
- Expose checkPermissionForSubject for in-process callers (scheduler)

No new PermissionAction values are added. Uses the existing stack:deploy,
node:manage, and system:settings matrix. Scoped Admiral grants work on the
exact (nodeId, stackName) target per SEN-438.

* test: add RBAC coverage for scheduled operations authorization

* test: update frontend tests for scheduled-ops RBAC gate changes

- useStackMenuItems: gate Schedule task on canDeploy not isAdmin;
  add test for canDeploy=true, non-admin case
- buildNavigationModel: default scheduledOpsAccessible to true
  (default ctx is admin, who can always schedule)
- useViewNavigationState: add stack:deploy to admin can() mocks
  so canScheduleAny resolves correctly

* fix: keep checkPermission unchanged, add checkPermissionForSubject standalone

The earlier refactoring that made checkPermission delegate to
checkPermissionForSubject changed the call order of effectiveTier(req)
relative to the admin bypass, which subtly broke the Community tier
clamping in the audit-log route. Keep checkPermission byte-identical
to the original and expose checkPermissionForSubject as a standalone
function used only by the scheduler revalidation path.

* fix: prefix unused selectorType parameter in resolveTaskPermissionScope

* fix: address audit findings — existence oracle, revalidation test, action filtering

Three corrections from the independent PR audit:

RC-1 (action filtering): Wire canScheduleActionAnywhere into the action
picker in ScheduledOperationsView so actions the user can never schedule
(prune without system:settings, node:manage actions without a scoped
grant) are filtered from the picker entirely. Add canScheduleAction
check on the Create button against the currently selected target, so
the submit button is disabled when the caller cannot schedule the
chosen action on the chosen target.

RC-2 (existence oracle): Six by-ID endpoints (GET /:id, DELETE /:id,
PATCH /:id/toggle, POST /:id/run, GET /:id/runs/export, GET /:id/runs)
now return a uniform 404 when permission is denied on an existing task,
so an unauthorized caller cannot distinguish "task does not exist" from
"task exists but you are not authorized." Added
requireTaskExistsPermission helper for the 404 variant; POST create and
PUT merged-scope checks keep requireTaskPermission (403).

RC-3 (revalidation test): Fixed the orphan-creator test to assert the
post-execution task state (auto-disabled, explicit error message) rather
than wrapping executeTask in a try/catch with a no-op else branch, since
executeTask catches TaskAuthorizationError internally and returns
normally.

Added canScheduleActionAnywhere helper and AuthContext mock to
ScheduledOperationsView tests (38/38 pass).

* fix: remove unused TaskAuthorizationError import from test

* fix: use 404 on PUT phase-1 unauthorized-access check

The PUT two-phase check's first phase (ownership verification)
now uses requireTaskExistsPermission (404) instead of a manual
403, consistent with the six other by-ID endpoints. This prevents
a caller from probing task-ID existence via the PUT route.

* fix: address QA findings — reorder prechecks, surface permission reason

Two live-confirmed fixes from the 3-node fleet QA pass:

Finding #4 (offline-node error ordering): Swap the order of the node-
reachability precheck and the creator-permission revalidation in
SchedulerService.executeTask. Authorization now runs first, so a
revoked grant is always surfaced as an explicit auto-disable with a
clear error message, even when the target node is offline. Previously
the reachability check ran first, hiding the revocation behind a
misleading "target node is offline" error and leaving the task enabled
indefinitely while the node was down.

Finding #7 (unexplained Save dead-end): When the Create/Update button
is disabled because canScheduleAction denies the selected action-target
combination, a muted text line now appears below the button:
"You do not have permission to schedule this action on the selected
target." This gives scoped users meaningful feedback instead of a
silently disabled button with no explanation.

* chore: remove redundant !!formName guards

The earlier short-circuit conditions in isSaveDisabled and
saveDisabledReason already guarantee formName is truthy by the
time the canSaveWithCurrentTarget check runs. The !!formName
guard is a no-op — flagged by GitHub code-quality as 'Useless
conditional: This negation always evaluates to true.'
2026-08-02 01:15:37 -04:00
Anso 10fb93dcb1 feat(scheduler): schedule container restart, stop, and start (#1526)
* feat(scheduler): schedule container restart, stop, and start

Add container as a scheduled-task target type so operators can automate lifecycle actions against standalone containers by node and name, with matching UI pickers, validation, execution on local and remote nodes, and tests.

* fix(scheduler): stack service matching and container picker hygiene

Backfill Service on smartFallback containers so per-service stack restarts work when container_name is set. Match services by compose label and container name in stack routes and scheduled restarts. Exclude Sencho from GET /api/containers lists. Hide the Restart Stack service picker when a stack has only one service.

* test(scheduler): scope service checkbox assertion to Services block

The create dialog also has a Delete after run checkbox. Count checkboxes only inside the Services section so CI does not include unrelated form controls.

* fix(scheduler): narrow closest() result to HTMLElement in schedule test

The service-checkbox assertion passed an Element from closest() into
within(), which requires an HTMLElement, failing tsc -b in the frontend
build and Docker build stages. Use the closest<HTMLElement>() type
argument so the value type-checks without an unsafe cast.

* fix(scheduler): hide Sencho container on remote node picker lists

Remote container lists are proxied from peer Sencho instances, so id-only self filtering missed peers on older builds. Await SelfIdentity init, match ImageID, and drop official saelix/sencho images. Apply the same heuristic in the scheduled-operations UI and when the hub fetches remote containers for scheduled runs.

* test(monitor): add missing DatabaseService mocks for scan history cleanup

* test(scheduler): add missing markStaleScansAsFailed mock

SchedulerService.tick() calls db.markStaleScansAsFailed() to sweep stale
vulnerability scans. The scheduler-service test was missing this method in
its DatabaseService mock, causing TypeError failures during test initialization.

Added mockMarkStaleScansAsFailed to hoisted mocks and DatabaseService mock
object, returning safe default of 0 scans marked as failed.

* test(compose): add missing FileSystemService mocks for getStackContent/getEnvContent

* test(containers-route): mock SelfIdentityService to prevent initialize() crash

The excludeSelfContainers() helper calls SelfIdentityService.initialize(), which tries to access DockerController. Without a proper SelfIdentityService mock, the initialize() call fails silently, causing a 500 error on GET /api/containers.

Added SelfIdentityService mock with initialize(), isOwnContainer(), and isOwnImage() methods to prevent the crash.
2026-07-02 22:31:29 -04:00
Anso bc8c051962 feat(scheduler): consistent action targeting in Scheduled Operations (#1431)
Give every scheduled action an explicit, predictable target model
(Action then Node then Stack then Options then Schedule):

- System Prune now exposes a Node picker and requires a node, so it can
  no longer run silently on the default node.
- Vulnerability Scan and System Prune list local nodes only; both run on
  the hub-local Docker daemon and reject remote nodes on the backend.
- Restart Stack service discovery loads services from the selected node
  via fetchForNode instead of the active or local node.
- Fleet Snapshot shows a read-only "Scope: Entire fleet" summary.

Backend gains a shared local-node guard and prune node validation on
create and update, plus an executor-level remote-node guard, so the
frontend and backend validation now agree for every action.
2026-06-24 21:02:15 -04:00
Anso 0af7ad1df2 refactor(scheduler): drive scheduled-action metadata from a shared registry (#1428)
Scheduled-operation action metadata was duplicated across the backend route
validator, the DatabaseService action union, the desktop action picker, the
Timeline lanes, and the mobile labels/tones. Adding or renaming one action meant
editing all of them.

Introduce one registry per package as the single source within that package:

- backend/src/services/scheduledActionRegistry.ts owns the action list and
  target-type validation; routes/scheduledTasks.ts and DatabaseService import
  from it (BackendScheduledAction type, VALID_ACTIONS, validateActionTarget).
- frontend/src/lib/scheduledActions.ts owns the UI metadata (labels, short
  labels, categories, tones, target/node/stack/service flags, helper text) and
  drives the create-flow picker, the All Tasks label, the Timeline lanes, and
  the mobile schedule view.

Timeline lanes now group by semantic category (Lifecycle, Updates, Security,
Maintenance, Backups) sourced from the registry. The update-fleet UI alias is
made explicit via a backendAction field. Backend validation stays authoritative;
parity tests on each side keep the action sets in lockstep.
2026-06-24 20:09:13 -04:00