Skip to content

VPR-59 [4/4] CMS: delegated block editing via per-block edit permissions#255

Open
rlorenzo wants to merge 3 commits into
VPR-59-cms-r3-frontendfrom
feature/cms-delegated-editing
Open

VPR-59 [4/4] CMS: delegated block editing via per-block edit permissions#255
rlorenzo wants to merge 3 commits into
VPR-59-cms-r3-frontendfrom
feature/cms-delegated-editing

Conversation

@rlorenzo

@rlorenzo rlorenzo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Slice 4 of the CMS migration stack (stacks on #253; the diff shows only this feature). Adds delegated block editing: admins attach RAPS permissions to a block as its edit permissions, and holders of any of them can edit that block's content and attached files — without SVMSecure.CMS.ManageContentBlocks and without access to anything else.

⚠️ Manual schema change required (before deploying to each environment)

CREATE TABLE dbo.ContentBlockToEditPermission (
    ContentBlockEditPermissionID int IDENTITY(1,1) NOT NULL
        CONSTRAINT PK_ContentBlockToEditPermission PRIMARY KEY,
    ContentBlockID int NOT NULL
        CONSTRAINT FK_ContentBlockToEditPermission_ContentBlock REFERENCES dbo.ContentBlock (contentBlockID),
    permission varchar(500) NOT NULL
);

CREATE INDEX IX_ContentBlockToEditPermission_ContentBlockID
    ON dbo.ContentBlockToEditPermission (ContentBlockID) INCLUDE (permission);

Apply to dev → TEST → PROD ahead of the corresponding deploy (same manual-DDL process as VPR-143). Additive only; invisible to legacy ColdFusion (which reads only the view-permission table), so coexistence is unaffected.

Semantics

  • Edit access to block B = ManageContentBlocks or any of B's edit permissions (ANY-OF).
  • An empty edit list means manager-only — delegation is explicit (deliberately not the view list's "empty ⇒ all SVMSecure" rule).
  • Delegated editors CAN: edit content, attach existing managed files (capped, minimal-DTO search), upload new files into the block's folder (they inherit the block's view permissions, and the editor can attach them even without holding those permissions), detach files, view/load version history, save (history attribution + 409 stale-edit guard unchanged).
  • Delegated editors CANNOT: rename (title or friendly name), change system/section/page/order/public access, change either permission list, delete/restore, reach the manage list/history pages, or touch the file store beyond the block-scoped operations above.
  • Soft-deleted blocks are not editable by delegates.

Implementation

  • New ContentBlockToEditPermission entity/table + CanEditAsync authorization (single-resolve permission HashSet, fail-closed).
  • Content-only PATCH gains attachment deltas; server-side field whitelisting is the enforcement boundary (the delegated DTO simply has no settings fields).
  • New content-scoped endpoints so the editor has ONE code path for all users: GET /editable, GET /attachable-files, per-block files/check-name, POST {id}/files, rollback-only DELETE {id}/files/{guid} (owner + folder + not-attached-elsewhere constrained).
  • Attach guard: newly-attached files must pass the same download-access check as the search, with one exception — a file the current user uploaded into this block's own folder (uploader + folder match, mirroring the rollback-delete rule). This lets a delegate attach an inline upload whose inherited view permission they don't hold, while blocking a restricted file they uploaded for a different folder from being attached onto a broader-visibility block.
  • Editor renders a capability mode: read-only settings summary for delegates; managers additionally get the "Edit access" selector. Hub gains a "Blocks you can edit" card for delegates.
  • Public fn endpoint DTO unchanged (no edit-permission disclosure).
  • Tests: authorization matrix, field-scoping, attachment deltas, rollback-delete constraints, attach uploader/folder exception (in-folder allowed, cross-folder rejected), editable/attachable filtering, capability-mode rendering, hub card, PATCH conflict flow.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds “delegated block editing” to the CMS migration stack by introducing per-block edit-permission lists (separate from view permissions) and a content-scoped editing/file workflow that allows non-managers to edit only content + attachments for blocks they’re delegated.

Changes:

  • Introduces ContentBlockToEditPermission (entity + EF mapping) and edit-authorization in CmsContentBlockService (CanEditAsync, editable-block listing, attachable-file search, rollback-delete eligibility).
  • Expands the CMS content API with content-scoped endpoints for delegated editors (editable blocks list, attachable-files search, block-scoped file check-name/upload/rollback-delete) and adds attachment deltas to the content-only PATCH.
  • Updates the Vue CMS editor and hub to support “capability mode” (manager vs delegated UI) and adds/updates frontend + backend tests for delegated scenarios.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
web/Models/VIPER/ContentBlockToEditPermission.cs New EF entity representing a block → edit-permission row.
web/Models/VIPER/ContentBlock.cs Adds navigation collection for edit-permission rows.
web/Classes/SQLContext/VIPERContext.cs Registers DbSet + model configuration for the new table.
web/Areas/CMS/Services/CmsContentBlockService.cs Core authorization + delegated editing behaviors (content-only update with attachment deltas, editable listing, attachable-file search, rollback-delete checks).
web/Areas/CMS/Models/DTOs/ContentBlockDto.cs Adds EditPermissions to block DTO; adds minimal CmsAttachableFileDto.
web/Areas/CMS/Models/CmsContentBlockMapper.cs Maps edit-permission list into DTO.
web/Areas/CMS/Models/CMSBlockAddEdit.cs Adds EditPermissions to create/update request model.
web/Areas/CMS/Controllers/CMSContentController.cs Widens selected endpoints to SVMSecure + CanEditAsync gate; adds content-scoped endpoints for delegated file attach/upload/rollback-delete.
VueApp/src/CMS/types/index.ts Adds editPermissions, plus types for editable blocks and attachable file search results.
VueApp/src/CMS/router/routes.ts Broadens content-block edit route permission to SVMSecure (server is source of truth).
VueApp/src/CMS/pages/ContentBlockEdit.vue Implements manager vs delegated capability UI; uses PATCH for delegated saves; switches attach/search and rollback paths to content-scoped APIs.
VueApp/src/CMS/pages/CmsHome.vue Adds “Blocks you can edit” hub card (delegated editors).
VueApp/src/CMS/components/PermissionSelector.vue Makes hint configurable to reuse for edit-permissions selector.
VueApp/src/CMS/components/InlineFileUpload.vue Adds block-scoped upload mode (check-name/upload/rollback-delete) for delegated editors.
VueApp/src/CMS/tests/inline-file-upload.test.ts Tests block-scoped uploader behavior (no folder/permission fields; no use-existing; POST overwrite).
VueApp/src/CMS/tests/content-block-edit-save.test.ts Updates save payload expectations; tests edit-access selector; updates rollback URL expectations; updates attachable-files behavior.
VueApp/src/CMS/tests/content-block-edit-delegated.test.ts New delegated-mode editor tests (read-only settings, PATCH save, conflict handling).
VueApp/src/CMS/tests/cms-home.test.ts New tests for delegated editable-blocks hub card behavior.
test/CMS/CMSContentControllerTests.cs Extends controller tests for CanEditAsync gating + content-scoped file ops.
test/CMS/CmsContentBlockServiceTests.cs Adds service-level tests for delegated authorization, attachment deltas, editable listing, attachable search, and rollback-delete eligibility.

Comment thread VueApp/src/CMS/pages/ContentBlockEdit.vue Outdated
Comment thread web/Areas/CMS/Controllers/CMSContentController.cs Outdated
Comment thread VueApp/src/CMS/pages/ContentBlockEdit.vue Outdated
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Delegated CMS editors can discover assigned blocks, edit content without manager settings access, replace attachments, and use block-scoped file APIs. Backend permission storage, authorization, controller endpoints, frontend flows, and tests were updated accordingly.

Changes

Delegated CMS editing

Layer / File(s) Summary
Edit permissions and service authorization
web/Models/..., web/Areas/CMS/Services/..., web/Classes/SQLContext/...
Adds persisted edit permissions and service logic for delegated authorization, attachment validation, content-only updates, and rollback eligibility.
Editor API gates and block-scoped files
web/Areas/CMS/Controllers/...
Adds per-block authorization to content, history, discovery, attachment search, upload, and rollback endpoints.
Delegated editor UI and upload routing
VueApp/src/CMS/pages/..., VueApp/src/CMS/components/..., VueApp/src/CMS/types/..., VueApp/src/CMS/router/...
Adds editable-block discovery, delegated read-only settings, content-only saves, manager edit access, attachable-file search, and block-scoped uploads.
Backend authorization and file-operation coverage
test/CMS/...
Tests delegated authorization, status handling, attachment validation, editable-block discovery, attachable-file filtering, and rollback safety.
Delegated editing and upload coverage
VueApp/src/CMS/__tests__/*
Tests delegated discovery, controls, save payloads, conflicts, attachment search, and scoped upload behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: delegated CMS block editing via per-block edit permissions.
Description check ✅ Passed The description matches the changeset and accurately describes delegated block editing and the schema change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/cms-delegated-editing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Comment thread web/Areas/CMS/Controllers/CMSContentController.cs Outdated
Comment thread web/Classes/SQLContext/VIPERContext.cs
Comment thread web/Areas/CMS/Services/CmsContentBlockService.cs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Comment thread web/Areas/CMS/Services/CmsContentBlockService.cs
Comment thread web/Classes/SQLContext/VIPERContext.cs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comment thread web/Areas/CMS/Services/CmsContentBlockService.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comment thread VueApp/src/CMS/router/routes.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Comment thread web/Areas/CMS/Services/CmsContentBlockService.cs
Comment thread web/Areas/CMS/Services/CmsContentBlockService.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

@rlorenzo rlorenzo changed the title [4/4] CMS: delegated block editing via per-block edit permissions VPR-59 [4/4] CMS: delegated block editing via per-block edit permissions Jul 6, 2026
@rlorenzo
rlorenzo force-pushed the VPR-59-cms-r3-frontend branch from f1ca704 to c7bd327 Compare July 10, 2026 07:57
@rlorenzo
rlorenzo force-pushed the feature/cms-delegated-editing branch from 26628ea to 45255eb Compare July 10, 2026 07:57
@rlorenzo
rlorenzo force-pushed the VPR-59-cms-r3-frontend branch from c7bd327 to bdacb1a Compare July 10, 2026 08:28
@rlorenzo
rlorenzo force-pushed the feature/cms-delegated-editing branch from f18972b to 889a28f Compare July 17, 2026 07:51
@rlorenzo
rlorenzo requested a review from Copilot July 17, 2026 07:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 2 comments.

Comment thread web/Classes/Utilities/TransactionHelper.cs Outdated
Comment on lines +699 to +726
public async Task<bool> RollbackDeleteFileAsync(Guid fileGuid, int contentBlockId, CancellationToken ct = default)
{
var entity = await LoadFileAsync(fileGuid, tracking: true, ct);
if (entity == null || entity.DeletedOn != null)
{
return false;
}
// Recheck immediately before deleting, under Serializable isolation, so the check and
// the delete commit as one atomic unit: SQL Server range-locks what this transaction
// read, so a concurrent attach to a different block either commits first (and this
// check then sees it) or blocks until this transaction finishes — it can no longer
// slip in between the check and the save and have its attachment deleted from under it.
return await ExecuteInTransactionAsync(async token =>
{
bool attachedElsewhere = await _context.ContentBlockToFiles
.AnyAsync(cbf => cbf.FileGuid == fileGuid && cbf.ContentBlockId != contentBlockId, token);
if (attachedElsewhere)
{
return false;
}
entity.DeletedOn = DateTime.Now;
entity.ModifiedOn = DateTime.Now;
entity.ModifiedBy = CurrentLoginId();
_audit.Audit(entity, CmsFileAuditActions.DeleteFile, "File Marked for Deletion");
await _context.SaveChangesAsync(token);
return true;
}, IsolationLevel.Serializable, ct);
}
@rlorenzo
rlorenzo force-pushed the VPR-59-cms-r3-frontend branch from 7d080e4 to aae003d Compare July 17, 2026 08:13
@rlorenzo
rlorenzo force-pushed the feature/cms-delegated-editing branch from 889a28f to 870f619 Compare July 17, 2026 08:13
@rlorenzo
rlorenzo requested a review from Copilot July 17, 2026 08:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.

@rlorenzo
rlorenzo force-pushed the VPR-59-cms-r3-frontend branch from aae003d to 0c04207 Compare July 17, 2026 08:27
@rlorenzo
rlorenzo force-pushed the feature/cms-delegated-editing branch from 870f619 to 80050b5 Compare July 17, 2026 08:27
@rlorenzo
rlorenzo requested a review from Copilot July 17, 2026 08:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 2 comments.

Comment on lines +714 to +719
bool attachedElsewhere = await _context.ContentBlockToFiles
.AnyAsync(cbf => cbf.FileGuid == fileGuid && cbf.ContentBlockId != contentBlockId, token);
if (attachedElsewhere)
{
return false;
}
Comment on lines +541 to +544
// Never trash a file another block still attaches — that would break the other block.
bool attachedElsewhere = await _context.ContentBlockToFiles
.AnyAsync(cbf => cbf.FileGuid == fileGuid && cbf.ContentBlockId != contentBlockId, ct);
return !attachedElsewhere;
rlorenzo added 2 commits July 17, 2026 01:43
Admins attach RAPS permissions to a content block as its EDIT
permissions; holders of any of them can edit that block without
SVMSecure.CMS.ManageContentBlocks.

- New ContentBlockToEditPermission entity/table (schema applied
  manually per environment; DDL in the PR description). An empty
  edit list means manager-only: delegation is explicit, deliberately
  not the view list "empty means all SVMSecure" rule
- CanEditAsync (manager OR edit-permission intersection, fail-closed,
  single-resolve HashSet) gates block load, history, diff, and the
  content-only PATCH, which now also carries attachment deltas; the
  delegated DTO has no settings fields, so field whitelisting is
  server-side by construction
- Content-scoped file endpoints (attachable search capped at 25 with
  a minimal DTO, per-block check-name/upload, rollback-only delete
  constrained to uploader+folder+not-attached-elsewhere) give the
  editor one code path for managers and delegates alike
- Editor renders a capability mode: read-only settings summary for
  delegates, full editor plus a new "Edit access" selector for
  managers; the hub gains a "Blocks you can edit" card; the editor
  route relies on server enforcement
- Delegates can view and load version history (restoring is a content
  save); soft-deleted blocks are not delegate-editable; the anonymous
  fn endpoint discloses nothing new
- 48 new tests (authorization matrix, field scoping, attachment
  deltas, rollback constraints, capability-mode rendering, hub card)
…of the default hint

- PermissionSelector's hint prop now accepts null to suppress the
  default hint text entirely (no reserved bottom space), instead of
  every consumer being stuck with "Users need any one of the selected
  permissions"
- Left nav edit hoists that copy to a single explanatory line above the
  item list instead of repeating it under every item's selector
@rlorenzo
rlorenzo force-pushed the VPR-59-cms-r3-frontend branch from 0c04207 to faa3eb1 Compare July 17, 2026 08:45
@rlorenzo
rlorenzo force-pushed the feature/cms-delegated-editing branch from 80050b5 to 192b05e Compare July 17, 2026 08:45
@rlorenzo
rlorenzo requested a review from Copilot July 17, 2026 08:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.

// snapshot taken before the transaction and the save at the end of it.
return await ExecuteInTransactionAsync(async token =>
{
var entity = await LoadFileAsync(fileGuid, tracking: true, token);
Comment on lines +516 to +520
var file = await _context.Files
.AsNoTracking()
.Where(f => f.FileGuid == fileGuid)
.Select(f => new { f.ModifiedBy, f.Folder })
.FirstOrDefaultAsync(ct);
.ToListAsync(ct);
if (files.Count != fileGuids.Count)
{
throw new ArgumentException("One or more files cannot be attached because they are deleted.");
// deleted out from under it. A false result means it became shared/gone in that window.
return await _fileService.RollbackDeleteFileAsync(fileGuid, contentBlockId, ct)
? NoContent()
: Conflict("The file could not be rolled back; it may now be attached to another block.");
@rlorenzo
rlorenzo force-pushed the feature/cms-delegated-editing branch from 192b05e to cb5b979 Compare July 17, 2026 08:59
@rlorenzo
rlorenzo requested a review from Copilot July 17, 2026 09:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 3 comments.

Comment on lines +367 to +385
// Full manage overrides everything (managers edit any block via the list page).
if (_userHelper.HasPermission(_rapsContext, currentUser, CmsPermissions.ManageContentBlocks))
{
return true;
}

// Delegated editing is explicit: an empty edit list is manager-only (NOT the view-list's
// empty-means-all rule), and a soft-deleted block is never editable by a delegate.
if (block.DeletedOn != null || block.EditPermissions.Count == 0)
{
return false;
}

// Resolve the user's permissions once into a case-insensitive set, mirroring
// Data.CMS.CheckFilePermission, then intersect with the block's edit permissions.
var userPermissions = _userHelper.GetAllPermissions(_rapsContext, currentUser)
.Select(p => p.Permission)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
return block.EditPermissions.Any(p => userPermissions.Contains(p));
Comment on lines +396 to +410
// Delegated matches only, as documented: managers work from the full list page, and
// the hub's "Blocks you can edit" card is manager-hidden and expects empty here.
if (_userHelper.HasPermission(_rapsContext, currentUser, CmsPermissions.ManageContentBlocks))
{
return new List<ContentBlockDto>();
}

// Single-resolve: the user's permissions are read once and reused for every block.
var userPermissions = _userHelper.GetAllPermissions(_rapsContext, currentUser)
.Select(p => p.Permission)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
if (userPermissions.Count == 0)
{
return new List<ContentBlockDto>();
}
Comment on lines +439 to +458
var block = await _blockService.GetContentBlockAsync(contentBlockId, ct);
if (block == null)
{
return NotFound();
}
// The section path IS the upload folder; refuse the upload when the block has none
// (mirrors CheckBlockFileName) rather than falling back to the storage root.
if (string.IsNullOrEmpty(block.ViperSectionPath))
{
return BadRequest("The content block has no VIPER section path to upload into.");
}

var request = new CmsFileCreateRequest
{
Folder = block.ViperSectionPath,
AllowPublicAccess = block.AllowPublicAccess,
Permissions = block.Permissions,
FileName = form.FileName,
Overwrite = form.Overwrite
};
…nt attach

- RollbackDeleteFileAsync's "not attached elsewhere" recheck and the
  soft-delete it guards were two separate statements with no
  transactional isolation, leaving a race window where a concurrent
  attach to a different block could land in the gap and get stranded
  by the delete anyway
- Wrap both in a Serializable transaction via a shared TransactionHelper
  (also usable by other services with the same needs); tests override
  the new virtual ExecuteInTransactionAsync hook to bypass transactions,
  since the EF in-memory provider doesn't support them
@rlorenzo
rlorenzo force-pushed the feature/cms-delegated-editing branch from cb5b979 to 87cecc9 Compare July 17, 2026 09:15
@rlorenzo
rlorenzo requested a review from Copilot July 17, 2026 09:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants