Skip to content

feat(authz): add scope selection step to the Role Assignment Wizard#111

Merged
jacobo-dominguez-wgu merged 18 commits intoopenedx:masterfrom
eduNEXT:bc/add-role-assignment-wizard-2
Apr 23, 2026
Merged

feat(authz): add scope selection step to the Role Assignment Wizard#111
jacobo-dominguez-wgu merged 18 commits intoopenedx:masterfrom
eduNEXT:bc/add-role-assignment-wizard-2

Conversation

@bra-i-am
Copy link
Copy Markdown
Contributor

@bra-i-am bra-i-am commented Mar 27, 2026

Implements Step 2 — Define Application Scope of the Assign Role Wizard, allowing users to select which courses and/or libraries a role assignment should apply to.

Additional Information

Resolves #92

Screencast

Screencast.from.21-04-26.14.06.24.webm

What's included

API layer (data/api.ts)

  • getScopes() — paginated endpoint with search, org, context_type, and management_permission_only filters.
  • getOrganizations() — fetches available organizations for the filter dropdown.

Hooks (data/hooks.ts)

  • useScopes — infinite-query hook with page-based pagination (parses page from the next URL via getNextPageParam).
  • useOrgs — standard query with 30-min stale time.
  • useAssignTeamMembersRole — invalidates teamMembersAll, permissionsByRole, userRoles, and allRoleAssignments caches on success.

data/hooks.ts (global, @src/data/hooks.ts)

  • useValidateUserPermissionsuseSuspenseQuery wrapper around the permissions validation endpoint; used by useScopePermissions to check per-org access.

DefineApplicationScopeStep / new components

  • ScopeFilterBar — debounced search input (300 ms) + organization filter dropdown; active org filter shown as a Paragon Badge chip.
  • ScopeList — scopes grouped by organization via collapsible OrgSection sections; infinite scroll via IntersectionObserver with loading spinners for initial load and paginated fetches.
  • useScopeListData — data-preparation hook that composes scope groups and injects per-org aggregate items (course-v1:{org}+* / lib:{org}:*) filtered by the user's management permissions; platform aggregate item is gated on hasPlatformPermission.
  • useScopePermissions — calls useValidateUserPermissions with one request per org (using the appropriate MANAGE_COURSE_TEAM or MANAGE_LIBRARY_TEAM action and wildcard scope) and maps the results into orgHasPermission: Record<string, boolean>; hasPlatformPermission is hardcoded to false pending backend support for scope-less permission checks.

LibrariesUserManager

  • Replaced the now-redundant AssignNewRoleTrigger component with a direct navigation button to /authz/assign-role?scope=<libraryId>&users=<username>.

i18n & error handling

  • All new strings extracted to messages.ts and wrapped with intl.formatMessage.
  • Role assignment errors now surface a localized toast; the user stays on Step 2 to retry.

How to test?

Prerequisites

  • A running instance of frontend-app-admin-console pointed at a backend that supports the /authz/scopes/, /authz/organizations/, and /api/v1/permissions/validate/ endpoints.
  • At least two organizations with courses and/or libraries.
  • Three test users: a regular user (no management permissions), an org-level manager (holds manage_course_team or manage_library_team on at least one org), and a second user without those permissions.

Manual test steps

  1. Navigate to the wizard — go to the Authz section and start assigning a role to a user. Confirm Step 1 (user selection) still works, then advance to Step 2 ("Where It Applies").
  2. Search — type a partial course/library name in the search bar. Verify the scope list filters in real time (with ~300 ms debounce) without losing already-selected scopes.
  3. Organization filter — select an org from the dropdown. Confirm only scopes belonging to that org appear and a badge chip shows the active filter. Clear it and confirm all scopes return.
  4. Org sections — verify scopes are grouped under collapsible org headers labeled Org: <orgName>. Collapse and expand a section to confirm it works.
  5. Multi-select — check multiple scopes across different orgs. Navigate back to Step 1 and return; confirm selections are preserved. Apply a search/filter and confirm selected items remain checked.
  6. Infinite scroll — if there are more scopes than fit in one page, scroll to the bottom of the list. Verify the spinner appears and the next page loads without resetting selections.
  7. Org-level aggregate (permitted user) — log in as the org-level manager. Confirm "All courses in this organization" (or "All libraries…") appears at the top of each org section where that user holds the management permission. Verify the wildcard scope (course-v1:<org>+* or lib:<org>:*) is submitted when that option is selected and saved.
  8. Org-level aggregate (unpermitted user) — log in as the regular user. Confirm the org aggregate option is absent for all orgs.
  9. Platform aggregate (disabled) — confirm that no "All courses/libraries in Platform" option appears for any user, including platform admins. This feature is pending backend support for scope-less permission checks.
  10. Submit — select at least one scope and click Save. Confirm the button shows a loading spinner while the request is in flight. On success, verify the success toast appears and the user is redirected back to the home view.
  11. Error handling — simulate a backend error (e.g., disconnect the network or mock a 500). Confirm the localized error toast appears and the user remains on Step 2 to retry.
  12. LibrariesUserManager — open a library's user management page and click the "Assign Role" button. Confirm it navigates directly to /authz/assign-role?scope=<libraryId>&users=<username> without the old AssignNewRoleTrigger modal.

Test Cases

Note

Test case 8 cannot be fully implemented yet because the backend needs a mechanism to integrate platform-wide permissions. useScopePermissions.ts hardcodes hasPlatformPermission = false until that constraint is lifted.

# Requirement Status Notes
1 Step 2 is titled "Where It Applies" messages['wizard.step.defineScope.title'] = 'Where It Applies'
2 Search bar to search scopes by name Text input in ScopeFilterBar.tsx with debounce
3 Organization filter next to search bar (same behavior as M2.5) Dropdown in ScopeFilterBar.tsx
4 Scopes organized by organization, with header showing org name OrgSection renders Org: {orgName} — includes an extra "Org:" prefix not in the spec
5 Each scope item has a checkbox; multiple scopes can be selected ScopeCheckboxItem + Set<string> in AssignRoleWizard.tsx
6 Users with manage_course_team at org level see "All courses in this organization" orgAggregateScopeItems is filtered by orgHasPermission from useScopePermissions, which calls useValidateUserPermissions with course-v1:{org}+* scopes
7 Users with manage_library_team at org level see "All libraries in this organization" Same as 6 — uses lib:{org}:* scopes
8 Users with platform-wide permissions see global scope options Platform aggregate disabled (hasPlatformPermission = false); platform-wide checks are not yet possible
9 Users without higher-scope permissions do not see these options Org aggregates correctly hidden when orgHasPermission[org] is false; platform aggregate hidden for all users (see item 8)
10 Scope list uses infinite scroll IntersectionObserver in ScopeList.tsx triggers fetchNextPage
11 Selections preserved when searching or filtering selectedScopes lives in AssignRoleWizard parent state, untouched by search/filter changes
12 Clicking Save shows loading state while request is in flight assignRoleMutation.isPending drives StatefulButton pending state with spinner
13 On success, toast appears and user redirected to previous view Toast fires and onClose() is called, delegating navigation to the caller
14 On error, existing error toast logic reused; user remains in Step 2 to retry showErrorToast called, step state unchanged on error

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Mar 27, 2026
@openedx-webhooks
Copy link
Copy Markdown

openedx-webhooks commented Mar 27, 2026

Thanks for the pull request, @bra-i-am!

This repository is currently maintained by @openedx/committers-frontend.

Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@github-project-automation github-project-automation Bot moved this to Needs Triage in Contributions Mar 27, 2026
@bra-i-am bra-i-am changed the title Bc/add role assignment wizard 2 feat: implement DefineApplicationScopeStep with scope/org filtering and aggregate permissions Mar 27, 2026
@bra-i-am bra-i-am changed the title feat: implement DefineApplicationScopeStep with scope/org filtering and aggregate permissions feat: add step 2 for the role assignment wizard Mar 27, 2026
@bra-i-am bra-i-am force-pushed the bc/add-role-assignment-wizard-2 branch from 9ff96b8 to b193f35 Compare March 27, 2026 19:43
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 27, 2026

Codecov Report

❌ Patch coverage is 98.43750% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.34%. Comparing base (913b88d) to head (f710eb2).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
src/authz-module/data/hooks.ts 94.44% 1 Missing ⚠️
...odule/role-assignation-wizard/AssignRoleWizard.tsx 96.87% 1 Missing ⚠️
...e/role-assignation-wizard/components/ScopeList.tsx 96.96% 1 Missing ⚠️
.../role-assignation-wizard/hooks/useScopeListData.ts 97.77% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #111      +/-   ##
==========================================
+ Coverage   96.70%   97.34%   +0.63%     
==========================================
  Files          81       89       +8     
  Lines        1880     2106     +226     
  Branches      423      457      +34     
==========================================
+ Hits         1818     2050     +232     
+ Misses         59       53       -6     
  Partials        3        3              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jacobo-dominguez-wgu
Copy link
Copy Markdown
Contributor

@bra-i-am Could you rebase please?

@bra-i-am bra-i-am force-pushed the bc/add-role-assignment-wizard-2 branch from 11d9fa3 to 2a52e40 Compare April 21, 2026 18:58
@bra-i-am bra-i-am marked this pull request as ready for review April 21, 2026 18:58
@bra-i-am
Copy link
Copy Markdown
Contributor Author

@jacobo-dominguez-wgu, sorry, I had some issues solving the conflicts, but now this PR is ready for review

}

const ScopeCheckboxItem = ({ scope, checked, onToggle }: ScopeCheckboxItemProps) => (
<div className="mb-2">
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.

I just compared with the design and noticed that elements have a bigger spacing between them.

Suggested change
<div className="mb-2">
<div className="my-4">

{scope.displayName}
</Form.Checkbox>
{scope.description && (
<small className="d-block text-muted pl-4">{scope.description}</small>
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.

Suggested change
<small className="d-block text-muted pl-4">{scope.description}</small>
<small className="d-block text-muted font-italic pl-4">{scope.description}</small>

import { useMemo } from 'react';
import { useValidateUserPermissions } from '@src/data/hooks';
import { useOrgs } from '@src/authz-module/data/hooks';
import { CONTENT_LIBRARY_PERMISSIONS, CONTENT_COURSE_PERMISSIONS } from '../../constants';
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.

Use absolute path for this import.

@jacobo-dominguez-wgu
Copy link
Copy Markdown
Contributor

The toast now shows on the bottom left corner, was that intentional?
image

@@ -0,0 +1,79 @@
import { useMemo } from 'react';
Copy link
Copy Markdown
Contributor

@dcoa dcoa Apr 22, 2026

Choose a reason for hiding this comment

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

I don't see this hook being used. The intention is define it and integrate it later?

@dcoa dcoa changed the title feat(authz): add "Where It Applies" scope-selection step to the Role Assignment Wizard feat(authz): add scope selection step to the Role Assignment Wizard Apr 22, 2026
queryState,
platformAggregateScopeItem,
showOrgAggregates,
} = useScopeListData({ contextType, search: debouncedSearch, org: selectedOrgs[0] || '' });
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.

org: selectedOrgs[0] || '' 

It is this intentional?
The filter is presented as a multi-select checkbox but the filter is always applied to 1 organization if I choose 2or more the filter is not applied

Screencast.from.2026-04-22.16-43-52.webm

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ay verdad, thanks for the call. Yes, it is intentional... the backend still doesn't receive multiple orgs at that endpoint. At the moment, if we send all the orgs, it treats them as one string and looks for org1,org2 instead of treating them as separate orgs.

cc. @jacobo-dominguez-wgu

<Form.Control
type="text"
value={search}
onChange={(e: { target: { value: string } }) => onSearchChange(e.target.value)}
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.

Suggested change
onChange={(e: { target: { value: string } }) => onSearchChange(e.target.value)}
onChange={(e: ChangeEvent<HTMLInputElement>) => onSearchChange(e.target.value)}

@bra-i-am
Copy link
Copy Markdown
Contributor Author

bra-i-am commented Apr 22, 2026

The toast now shows on the bottom left corner, was that intentional? image

@jacobo-dominguez-wgu, it is not related to these changes; I just checked out to master, and that's the current behavior

Comment thread src/authz-module/index.scss Outdated
@@ -109,13 +116,21 @@
resize: vertical;
caret-color: #212529;
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.

Just a nit for a near future: Lets try to use design tokens on this file.

@jacobo-dominguez-wgu
Copy link
Copy Markdown
Contributor

jacobo-dominguez-wgu commented Apr 22, 2026

I have tested it and everything works well, nice job! Lets just wait for the resolution on the orgs filter and the All courses/libraries on the platform slack discussion.

@jesusbalderramawgu
Copy link
Copy Markdown
Contributor

The toast now shows on the bottom left corner, was that intentional? image

@jacobo-dominguez-wgu, it is not related to these changes; I just checked out to master, and that's the current behavior

this has been fixed in another PR
#137

Copy link
Copy Markdown
Contributor

@arbrandes arbrandes left a comment

Choose a reason for hiding this comment

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

This jumps out from the code:

src/authz-module/role-assignation-wizard/hooks/useScopePermissions.ts is not imported by any non-test file. Meanwhile useScopeListData.ts:69-83 gates both the platform-wide aggregate (*) and the per-org aggregates on getAuthenticatedUser()?.administrator === true.

This is the gap called out in rows 6-9 of the test matrix in the PR description. Is it going to be implemented later?

@bra-i-am
Copy link
Copy Markdown
Contributor Author

This jumps out from the code:

src/authz-module/role-assignation-wizard/hooks/useScopePermissions.ts is not imported by any non-test file. Meanwhile useScopeListData.ts:69-83 gates both the platform-wide aggregate (*) and the per-org aggregates on getAuthenticatedUser()?.administrator === true.

This is the gap called out in rows 6-9 of the test matrix in the PR description. Is it going to be implemented later?

@arbrandes, thanks for taking the time to review this

I made some changes to implement per-org aggregates using useScopePermissions.ts. I’ve also updated the test matrix in the PR description to reflect the latest state

The gap in row 8 related to the platform-wide aggregate is explained here by @rodmgwgu: https://openedx.slack.com/archives/C07PRCN6G0N/p1776869300085249

@bra-i-am
Copy link
Copy Markdown
Contributor Author

@rodmgwgu, @jacobo-dominguez-wgu, @jesusbalderramawgu I added per-org aggregates using useScopePermissions and useValidateUserPermissions to control whether the "All courses/libraries in this organization" checkbox is displayed.

Screencast.from.23-04-26.11.26.33.webm

Comment thread src/authz-module/role-assignation-wizard/hooks/useScopeListData.ts Outdated
const hasPlatformPermission = contextType === 'course'
? hasPlatformCoursePermission
: hasPlatformLibraryPermission;
// TODO: compute hasPlatformPermission from per-org permission requests once the backend
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This TODO is no longer current, I think

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right... I updated the message to keep the TODO. What do you think now?

Copy link
Copy Markdown

@rodmgwgu rodmgwgu left a comment

Choose a reason for hiding this comment

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

Tested in my local and works as described.

I discussed with product and the "Selections are preserved when the user searches or filters. Selections are not preserved when scrolling." requirement can be ignored, as it came from an outdated discussion.

Copy link
Copy Markdown
Contributor

@jacobo-dominguez-wgu jacobo-dominguez-wgu left a comment

Choose a reason for hiding this comment

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

LGTM! Great work @bra-i-am

@jacobo-dominguez-wgu jacobo-dominguez-wgu merged commit 2b3b480 into openedx:master Apr 23, 2026
6 checks passed
@github-project-automation github-project-automation Bot moved this from Waiting on Author to Done in Contributions Apr 23, 2026
@jesusbalderramawgu
Copy link
Copy Markdown
Contributor

I've tested as well and it works, thank you @bra-i-am

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

Labels

open-source-contribution PR author is not from Axim or 2U

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Task - RBAC AuthZ - US: M2.8 Assign role wizard - Step 2 scope selection

8 participants