amaannawab923 opened a new pull request, #45522:
URL: https://github.com/apache/superset/pull/45522
### SUMMARY
Lets an extension contribute a top-level page — a screen of its own, at its
own URL — as opposed to a view occupying a slot in a screen Superset already
ships.
An extension declares the page in its manifest and registers a component for
it at runtime:
```json
"contributions": {
"pages": [
{ "path": "/review-queue", "name": "Review queue",
"nav": { "label": "Review queue" }, "permission": "can_review" }
]
}
```
```ts
pages.registerPage({ path: '/review-queue', name: 'Review queue' },
ReviewQueue);
```
Both halves are needed: the manifest declaration is what the server reads to
register a URL rule, so a cold load resolves; the runtime registration supplies
the component. Registering a component for a path the manifest does not declare
is refused, so the two cannot drift.
#### Paths are checked, not namespaced
The path is the author's to choose, but not theirs to take. Before
registering a rule, the server checks the path against the routes the
deployment actually serves. Two checks, because neither subsumes the other:
- **Matching the live URL map.** A literal comparison against known routes
gets this wrong: `/oauth-authorized/<provider>` never string-matches the
request it would answer, so a page could take an OAuth callback. Asking the
router instead catches it, along with redirects (`strict_slashes`) and rules
answering only other methods.
- **Scanning rule templates for the same prefix.** A page owns everything
beneath its path, so a page at `/report` would shadow a core `/report/list`
through its sub-path rule. No single probe URL is guaranteed to hit a static
descendant, so the templates are read directly.
Registration runs after operator blueprints, the FAB auth views and every
core view, so the map it checks is complete. **It re-runs on every boot**,
which is the part that holds over time: routes Superset registers conditionally
— the OAuth callback exists only under `AUTH_TYPE = AUTH_OAUTH` — are absent
where the feature is off, so a page may legitimately take that path there.
Enabling the feature later puts the real rule in the map first, and the next
boot refuses the page loudly rather than leaving an extension in front of the
callback.
A refused page is skipped, not fatal. One extension claiming a taken path
should not stop the server starting.
Two further layers back this up. The client route is a catch-all pushed
last, so `<Switch>` gives every host screen precedence the same way Werkzeug
gives its static rules precedence — pinned by a test that derives host paths
from the route table, so a route added in the wrong place is covered the day it
is added. And the extensions API reports only pages that actually got a rule,
read back from the URL map, so a refused page cannot reach the menu or accept a
component.
#### Permissions
A page may name a permission. The host creates it against the extension when
the extension loads — not during `superset init`, because an extension is
installed long after a deployment is initialised and an administrator cannot
grant a permission that does not exist. It reads as `can review on acme.tools`
in the Roles screen, so an extension's permissions group together and two
extensions naming the same permission stay separate.
Omitting the field leaves the page open to any signed-in user, which is what
every page did before this field existed.
A user without the permission gets 403 rather than FAB's `has_access`
redirect to the login page — they are already signed in, so sending them to log
in again says nothing true. Login still comes first, so an anonymous visitor is
redirected rather than refused; 403 would disclose that the page exists to
someone who has not identified themselves.
The Admin role is given these permissions on creation, and the grant is
re-checked each boot so a role that lost it repairs itself. This is not a new
policy: `sync_role_definitions` builds Admin from every permission that is not
one of the four object-level ones, so `superset init` would grant it anyway —
just not until someone next ran init, which installing an extension does not
otherwise require. Correspondingly, a page naming one of `database_access`,
`catalog_access`, `schema_access` or `datasource_access` is refused, since
those are exactly what Superset excludes when deciding Admin, and such a page
would have its grant stripped at the next `superset init` with nothing in the
logs to explain it.
#### Also included
`useSubPath()` and `useNavigate()`, so a page can own sub-paths
(`/review-queue/42/edit`) and navigate within itself as a client-side
transition. These exist because `react-router-dom` is not in the Module
Federation share scope — an extension importing it gets a second module
instance whose context the host's `<Router>` never populates.
`@superset-ui/core` **is** added to the share scope, so a page gets
`SupersetClient` with working CSRF rather than hand-rolling fetches.
#### Open question for reviewers
Paths are flat and author-chosen. The alternative is a host-derived
namespace (`/extensions/<publisher>.<name>/<slug>`), which makes collisions
impossible by construction rather than by inspection, at the cost of long URLs.
Grafana (`/a/<plugin-id>/*`) and Mattermost (`/plug/<id>`) namespace; Kibana
(`/app/<id>`) does not.
Flat paths are deliberate here, but they carry three costs worth naming:
1. **Upgrade fragility.** If Superset later ships a route at a path an
extension holds, that page is refused at the next boot — loud, but broken.
2. **Install order decides collisions between extensions.** Detected and
logged, but the winner depends on config order.
3. **No attribution in the URL.** `/approvals` reads as part of Superset,
with nothing saying third-party code serves it.
Happy to move to a namespaced prefix if reviewers prefer it — the shape is
set in `pagePath` and the manifest's path pattern.
### BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
After: a contributed page at `/approvals` with its navigation entry,
alongside Dashboards / Charts / Datasets. A page whose permission the user
lacks is absent from the navigation bar and answers 403 on a direct load.
### TESTING INSTRUCTIONS
Automated:
```bash
pytest tests/unit_tests/extensions/ -q
cd superset-frontend && npx jest src/core/pages src/views/routes.test.tsx
src/extensions
```
Manually, with `ENABLE_EXTENSIONS` on and an extension declaring two pages
(one with a `permission`):
1. Load the page's path directly — it renders inside Superset's chrome.
2. Load a deep path beneath it (`/review-queue/42/edit`) — it renders, and
`useSubPath()` reports `/42/edit`.
3. Click the navigation entry — a client-side transition, not a full page
load.
4. Add a page declaring a path Superset already serves, e.g. `/welcome`. On
restart the log names the conflict, the page is not registered, and `/welcome`
still serves Superset's own screen.
5. As a user without the declared permission, the second page is absent from
the navigation bar and a direct load answers 403. Grant the permission on the
extension in the Roles screen and both work.
6. Revoke the permission from Admin and restart — it is granted again, and
the log says so.
### ADDITIONAL INFORMATION
- [ ] Has associated issue:
- [x] Required feature flags: `ENABLE_EXTENSIONS`
- [x] Changes UI
- [ ] Includes DB Migration (follow approval process in
[SIP-59](https://github.com/apache/superset/issues/13351))
- [ ] Migration is atomic, supports rollback & is backwards-compatible
- [ ] Confirm DB migration upgrade and downgrade tested
- [ ] Runtime estimates and downtime expectations provided
- [x] Introduces new feature or API
- [ ] Removes existing feature or API
No migration: the permissions these pages use are created through the
security manager when an extension loads, as FAB permissions are elsewhere.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]