Skip to content

feat(committees): add access-denied permission check on mailing list CTAs#1016

Open
MRashad26 wants to merge 7 commits into
mainfrom
feat/LFXV2-2252-committee-mailing-list-permission-check
Open

feat(committees): add access-denied permission check on mailing list CTAs#1016
MRashad26 wants to merge 7 commits into
mainfrom
feat/LFXV2-2252-committee-mailing-list-permission-check

Conversation

@MRashad26

Copy link
Copy Markdown
Contributor

Summary

  • + Add List button (committee settings): on click, performs a fresh getCommittee() call; if writer !== true, redirects to the lens-aware overview (/foundation/overview or /project/overview) with _notice=mailing-lists triggering the "Access Denied" toast — consistent with the meetings, votes, and surveys pattern from LFXV2-2252
  • + Create New List link (picker dialog): replaced [routerLink] navigation with an onCreateNewList() click handler that performs the same fresh committee check; closes the dialog and redirects on denial, or navigates to /mailing-lists/manage on success
  • committeeUid is now passed through the dialog's data object so the dialog can perform the check independently without relying on stale parent-component state

Related PRs

Test plan

  • As a committee Manager, click + Add List → picker opens normally
  • As a committee Manager, click + Create New List inside the picker → navigates to /mailing-lists/manage
  • Downgrade a Manager to Member, then click + Add List → redirected to project overview with "Access Denied" toast
  • Downgrade a Manager to Member, open picker via another path, click + Create New List → dialog closes, redirected to project overview with "Access Denied" toast
  • Foundation-lens user denied → redirects to /foundation/overview not /project/overview

🤖 Generated with Claude Code

On '+ Add List' click in committee settings, perform a fresh committee
fetch before opening the picker dialog; redirect to overview with
_notice=mailing-lists if the member no longer has writer access.

On '+ Create New List' click inside the picker dialog, perform the same
fresh committee check; close the dialog and redirect on denial, or
navigate to /mailing-lists/manage on success.

Both handlers use lens-aware redirect (foundation vs project overview)
consistent with the votes and surveys permission-check pattern.

Signed-off-by: Rashad <mrashad@contractor.linuxfoundation.org>
…V2-2252

Signed-off-by: Rashad <mrashad@contractor.linuxfoundation.org>
@MRashad26 MRashad26 requested a review from a team as a code owner June 23, 2026 13:21
Copilot AI review requested due to automatic review settings June 23, 2026 13:21
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The mailing list picker and create-new-list actions in committee settings now check committee writer permission before proceeding. Non-writers are redirected to the appropriate overview route with a mailing-lists notice.

Changes

Writer-gated mailing list flows

Layer / File(s) Summary
Permission gate in committee settings tab
apps/lfx-one/src/app/modules/committees/components/committee-settings-tab/committee-settings-tab.component.ts
Adds LensService injection and refactors openMailingListPicker() to fetch the committee via CommitteeService.getCommittee(), derive the overview route from the active lens, and either redirect non-writers with _notice=mailing-lists or open the dialog via openPickerDialog(committeeUid, projectUid, projectSlug), which configures the dialog and persists associations on close.
Create-new-list navigation in picker dialog
apps/lfx-one/src/app/modules/committees/components/mailing-list-picker-dialog/mailing-list-picker-dialog.component.ts, apps/lfx-one/src/app/modules/committees/components/mailing-list-picker-dialog/mailing-list-picker-dialog.component.html
Replaces RouterLink with programmatic Router navigation; injects CommitteeService and LensService, derives committeeUid and projectSlug from dialog config. A new onCreateNewList() method checks writer permission and either redirects non-writers to the overview or navigates writers to /mailing-lists/manage. The template's "Create New List" control becomes a button calling onCreateNewList().

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding access-denied permission checks to mailing list CTAs.
Description check ✅ Passed The description matches the implemented committee permission checks, redirect behavior, and dialog changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/LFXV2-2252-committee-mailing-list-permission-check

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

@github-actions

Copy link
Copy Markdown

🚀 Deployment Status

Your branch has been deployed to: https://ui-pr-1016.dev.v2.cluster.linuxfound.info

Deployment Details:

  • Environment: Development
  • Namespace: ui-pr-1016
  • ArgoCD App: ui-pr-1016

The deployment will be automatically removed when this PR is closed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a “fresh permission check on click” flow for committee mailing list CTAs to avoid stale committee.writer state, aligning committees → mailing-lists behavior with the existing access-denied redirect + _notice toast pattern used elsewhere.

Changes:

  • Add a fresh getCommittee() permission check before opening the mailing list picker dialog (“+ Add List”) and redirect to the lens-aware overview on denial.
  • Replace the picker dialog’s “Create New List” routerLink with a click handler that performs the same fresh permission check and redirects (or navigates to /mailing-lists/manage on success).
  • Pass committeeUid into the dialog via DynamicDialogConfig.data so the dialog can re-check permissions independently.

Reviewed changes

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

File Description
apps/lfx-one/src/app/modules/committees/components/mailing-list-picker-dialog/mailing-list-picker-dialog.component.ts Adds onCreateNewList() with a fresh committee permission check and redirect/navigation behavior.
apps/lfx-one/src/app/modules/committees/components/mailing-list-picker-dialog/mailing-list-picker-dialog.component.html Switches “Create New List” to a click handler (previously routerLink-based).
apps/lfx-one/src/app/modules/committees/components/committee-settings-tab/committee-settings-tab.component.ts Adds a fresh permission check before opening the picker dialog and passes committeeUid into dialog data.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Address review comments from @copilot-pull-request-reviewer:

- mailing-list-picker-dialog.component.html: replace <a> with
  <button type="button"> for keyboard accessibility (per @copilot)
- mailing-list-picker-dialog.component.ts: build denyParams inside
  next() so fresh.project_slug can be included in the redirect query
  params, matching the pattern in committee-settings-tab (per @copilot)

Resolves 2 review threads.

Signed-off-by: Rashad <mrashad@contractor.linuxfoundation.org>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/lfx-one/src/app/modules/committees/components/committee-settings-tab/committee-settings-tab.component.ts`:
- Around line 117-135: The async getCommittee() callback can execute after
component state changes, causing mixed snapshot data to be used when opening the
dialog or redirecting. Capture all necessary state values (committee properties,
lens, and navigation context) into local const variables at the start of the
function before the async operation begins, and ensure both the deny() callback
and openPickerDialog(committee.uid) call use only these captured local variables
rather than fresh signal reads that might return different values due to state
mutations during the async operation.

In
`@apps/lfx-one/src/app/modules/committees/components/mailing-list-picker-dialog/mailing-list-picker-dialog.component.html`:
- Around line 21-22: The anchor element with the onCreateNewList() click handler
lacks proper button semantics and is not keyboard-accessible. Replace the anchor
element with a button element using type="button", keeping the existing
(click)="onCreateNewList()" handler and the styling classes intact. This ensures
the Create New List control is properly focusable and activatable via keyboard
navigation, meeting the accessibility requirements for interactive controls in
the component.

In
`@apps/lfx-one/src/app/modules/committees/components/mailing-list-picker-dialog/mailing-list-picker-dialog.component.ts`:
- Around line 85-90: The deny flow in the mailing-list-picker-dialog component
does not preserve project context when navigating to the overview page. Modify
the deny function to extract the project information from the fetched committee
payload and add it as a query parameter to denyParams when the active lens is
'project' (not 'foundation'). This ensures project context is maintained in the
navigation URL, consistent with the settings-tab deny flow. Apply the same fix
to both the deny callback function and the confirm flow callback function to
ensure consistency across all navigation paths in this component.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6c2ac82a-e8aa-4584-8218-daa8cb5f4030

📥 Commits

Reviewing files that changed from the base of the PR and between 0118ec3 and 9515004.

📒 Files selected for processing (3)
  • apps/lfx-one/src/app/modules/committees/components/committee-settings-tab/committee-settings-tab.component.ts
  • apps/lfx-one/src/app/modules/committees/components/mailing-list-picker-dialog/mailing-list-picker-dialog.component.html
  • apps/lfx-one/src/app/modules/committees/components/mailing-list-picker-dialog/mailing-list-picker-dialog.component.ts

@MRashad26

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed

Commit: 84bdf58

Changes Made

  • mailing-list-picker-dialog.component.html: replaced <a> with <button type="button"> for native keyboard focusability (per @copilot-pull-request-reviewer)
  • mailing-list-picker-dialog.component.ts: moved denyParams construction inside next() so fresh.project_slug is available and conditionally added to the denial redirect, matching the same pattern in committee-settings-tab.openMailingListPicker() (per @copilot-pull-request-reviewer)

Threads Resolved

2 of 2 unresolved threads addressed in this iteration.

Address review comments from @coderabbitai:

- committee-settings-tab.component.ts: capture committeeUid and
  projectUid as local consts before the async getCommittee() call so the
  callback always operates on click-time snapshot values rather than
  potentially-stale signal reads (per @coderabbitai)
- committee-settings-tab.component.ts: add takeUntilDestroyed() to the
  getCommittee() pipe so the callback is cancelled if the component is
  destroyed mid-request (per @coderabbitai)
- committee-settings-tab.component.ts: pass projectUid into
  openPickerDialog() instead of re-reading this.committee().project_uid
  inside the method (per @coderabbitai)

Resolves 1 review thread.

Signed-off-by: Rashad <mrashad@contractor.linuxfoundation.org>
Copilot AI review requested due to automatic review settings June 23, 2026 14:01
@MRashad26

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed

Commit: 3cc6f1c

Changes Made

  • committee-settings-tab.component.ts: captured committeeUid and projectUid as click-time local consts before the async call so the callback never re-reads potentially-stale signals; added takeUntilDestroyed(this.destroyRef) to cancel the subscription on component destroy; openPickerDialog() now receives projectUid as a parameter (per @coderabbitai)

Threads Resolved

1 of 1 unresolved threads addressed in this iteration.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@MRashad26 All three changes look great — capturing the signal values as click-time local consts eliminates the stale-read risk, takeUntilDestroyed keeps the subscription lifecycle clean, and threading projectUid into openPickerDialog() properly carries the context through. Thanks for the thorough follow-through!

🐇✨

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 3 out of 3 changed files in this pull request and generated 2 comments.

Address review comments from @copilot-pull-request-reviewer:

- mailing-list-picker-dialog.component.ts: add takeUntilDestroyed() to
  onCreateNewList() pipe so the navigation callback is cancelled if the
  dialog is destroyed before the getCommittee() request resolves (per
  @copilot)
- committee-settings-tab.component.ts: tighten openPickerDialog()
  signature from string|undefined to string — Committee.project_uid is
  required in the shared interface, so the undefined arm and ?? ''
  fallback were unnecessary (per @copilot)

Resolves 2 review threads.

Signed-off-by: Rashad <mrashad@contractor.linuxfoundation.org>
@MRashad26

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed

Commit: e432494

Changes Made

  • mailing-list-picker-dialog.component.ts: added takeUntilDestroyed(this.destroyRef) to onCreateNewList() pipe — cancels the subscription if the dialog is destroyed before getCommittee() resolves (per @copilot-pull-request-reviewer)
  • committee-settings-tab.component.ts: tightened openPickerDialog() signature from string | undefined to string and removed ?? '' fallback — Committee.project_uid is required in the shared interface so the undefined arm was unreachable (per @copilot-pull-request-reviewer)

Threads Resolved

2 of 2 unresolved threads addressed in this iteration.

@luismoriguerra

Copy link
Copy Markdown
Contributor

Audit: PR #1016 — feat(committees): add access-denied permission check on mailing list CTAs

MRashad26 · feat/LFXV2-2252-committee-mailing-list-permission-checkmain · OPEN · 3 files, +86/−25 · LFXV2-2252

Scope: Fresh getCommittee() writer check before opening mailing-list picker and before Create New List navigation; committeeUid passed via dialog data.

Lanes run: yarn lint · yarn check-types · ui-reviewer · bugbot · secrets grep


1. Why this change

+ Add List and + Create New List relied on stale canEdit() / direct routerLink — same mid-session downgrade gap as meetings/votes/surveys.

2. How it was done

  • openMailingListPicker() — fresh check before dialog; captures committeeUid/projectUid at call start.
  • Dialog onCreateNewList() — independent re-check; closes dialog then navigates or denies.
  • Anchor → <button type="button"> for keyboard accessibility.

3. Is it correct

Yes — merge-ready. Pattern aligns with #997/#1000. _notice=mailing-lists registered in ACCESS_DENIED_MESSAGES. takeUntilDestroyed on async subscribes.

Prior review: Async state capture → fixed (3cc6f1c). Button a11y → fixed. Success-path project slug → fixed. Error-path project slug → still open (see inline). Save/remove while dialog open → no second check (same scope as sibling PRs; server enforces on PUT).

4. Missing / gaps

  1. Non-blocking — Error callback in onCreateNewList() navigates with { _notice: 'mailing-lists' } only — omits project slug that success-path deny includes (mailing-list-picker-dialog.component.ts:105-107).
  2. Non-blocking — Duplicated deny+check logic across settings tab and dialog — consider shared helper when a 5th copy lands.
  3. Non-blocking — Save association / Remove ML have no fresh writer re-check (entry CTAs only — consistent with feat(votes): add permission check on committee Create Vote CTA click #997/feat(surveys): add permission check on committee Create Survey CTA click #1000 scope).

5. Q&A this PR resolves

  • Why pass committeeUid into the dialog? — Dialog must re-check independently; parent state can go stale while open.
  • Why close dialog before deny navigate? — Avoids modal overlay on overview redirect.
  • Does /mailing-lists/manage route exist? — No explicit route; matches pre-PR routerLink (docs reference it; :id may treat "manage" as an id). Pre-existing, not introduced here.

6. Business rules & ADRs

  • Business rule: Both mailing-list entry CTAs re-fetch committee.writer before proceeding; denial uses _notice=mailing-lists with lens-aware overview redirect.
  • ADR status: None — extends existing _notice + writerGuard pattern.

7. Open questions & confirmations

  • (confirm) Is /mailing-lists/manage the intended create URL given routes define create and :id only? Pre-dates this PR.

Verdict: 🟡 Minor comments
Merge readiness: safe-to-merge (optional error-path project slug fix)

@luismoriguerra luismoriguerra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Audit follow-up: one remaining non-blocking consistency nit on the error-path deny params (inline). Top-level audit report posted separately.

@ahmedomosanya ahmedomosanya left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Audit result: 🔴 Required changes

The PR references LFXV2-2252, but that is the parent Org Lens parity epic. The specific ticket appears to be LFXV2-2469 (“Allow committee writers to create/manage mailing lists for their committee”). LFXV2-2484 may also be relevant if this is intended to address the reported mailing-list 403 bug, but LFXV2-2469 is the closest feature ticket.

Blocking issues:

  • onCreateNewList() navigates to /mailing-lists/manage, but the router defines /mailing-lists/create and /:id. As written, manage is treated as a mailing-list id rather than the create page.
  • The create navigation passes project_uid, but writerGuard resolves write access from ?project=<slug> or active context. This should follow the same slug-based query-param contract used by the votes/surveys helper so guarded navigation works reliably.

Additional note:

  • openMailingListPicker() treats every getCommittee() failure as access denied, so transient 5xx/network failures can redirect valid writers with a misleading _notice=mailing-lists toast.

Local checks passed: yarn lint:check, yarn check-types, and yarn format:check.

Address @luismoriguerra review on PR #1016: the error() callback in
onCreateNewList() was redirecting with only { _notice: 'mailing-lists' },
while the success-path deny correctly included project_slug. On a transient
fetchCommittee failure, the toast would lose project context.

Fix: pass projectSlug through openPickerDialog() into the dialog config,
then construct the same denyParams shape in the error handler.

Resolves 1 review thread.

Signed-off-by: Rashad <mrashad@contractor.linuxfoundation.org>
Copilot AI review requested due to automatic review settings June 26, 2026 16:43
@MRashad26

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed

Commit: e0dade3

Changes Made

  • committee-settings-tab.component.ts: updated openPickerDialog() signature to accept projectSlug: string | undefined; passes committee.project_slug at the call site and includes it in the dialog config
  • mailing-list-picker-dialog.component.ts: reads projectSlug from dialog config; error handler now constructs the same denyParams shape ({ _notice: 'mailing-lists', project?: slug }) as the success-path deny

Threads Resolved

1 of 1 unresolved threads resolved.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 3 out of 3 changed files in this pull request and generated no new comments.

@dealako dealako left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@MRashad26 — clean implementation that closes out a well-iterated review cycle.

This follows the permission-check pattern established by meetings (#992), votes (#997), and surveys (#1000), and actually improves on the original meetings implementation by adding takeUntilDestroyed and lens-aware redirects — both of which committee-meetings is still missing.

All prior findings from Copilot (×2), CodeRabbit, and @luismoriguerra have been addressed across the five iteration commits. The snapshot-before-async pattern, tightened projectUid: string signature, proper <button type="button"> semantics, and project_slug in both deny paths are all confirmed correct in the current code.

Security review confirms no new risks: overviewPath is derived from an internal signal (no open-redirect risk), query params come from server-returned data (no injection risk), and the upstream microservice enforces the real authorization boundary via the user's bearer token.


🟡 Minor: 1 — test coverage gap for async permission-check paths
⚪ Nit: 1 — duplicate denyParams construction in two error() paths

Approved with minor comments

@@ -10,6 +10,7 @@ import { TagComponent } from '@components/tag/tag.component';
import { Committee, CreateMailingListRequest, GroupsIOMailingList, JoinMode, MailingListPickerDialogResult } from '@lfx-one/shared/interfaces';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[minor] No automated test coverage for the new async permission-check paths. The denial flow (writer !== true) and error fallback in both openMailingListPicker() and onCreateNewList() are only covered by the manual test plan.

The sibling implementations (meetings, votes, surveys) also lack these tests — so this is consistent with the current repo pattern. A Jasmine unit test mocking CommitteeService.getCommittee() to return a non-writer response would be the minimum addition if test coverage is required here; otherwise, document this as a known gap consistent with peer implementations.

private readonly projectSlug: string | undefined = this.config.data.projectSlug;

public selectedMailingListUids = signal<Set<string>>(new Set(this.config.data.associatedUids));
public mlSearchQuery = signal('');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] The error() handler builds its own errorDenyParams object rather than reusing the same construction as the next() deny path. The two blocks are nearly identical — the only difference is the source of the slug (this.projectSlug vs fresh.project_slug). A one-line pre-capture of the slug before subscribe (as done in the settings-tab) would unify both paths:

const projectSlug = this.projectSlug; // captured once
// ... then in both next-deny and error:
const denyParams: Record<string, string> = { _notice: 'mailing-lists' };
if (projectSlug) denyParams['project'] = projectSlug;

Not a functional issue — just a consistency note.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants