amaannawab923 opened a new pull request, #45515:
URL: https://github.com/apache/superset/pull/45515

   ### SUMMARY
   
   An extension can place a view inside a surface Superset already ships, but 
it cannot add a screen of its own. `ViewContributions` has no entry for a 
route, so an extension that needs a page — a settings screen, a queue, a report 
— has no way to ask for one.
   
   This adds one declaration that drives both halves of a page:
   
   ```json
   "contributions": {
     "pages": [{
       "id": "acme.demo.queue",
       "name": "Review queue",
       "path": "/acme-demo/queue/",
       "permission": { "resource": "Dashboard", "action": "read" }
     }]
   }
   ```
   
   The frontend mounts it as a client-side route via `views.registerPage`; 
`init_extensions` registers a view rendering the application shell at the same 
path.
   
   **The backend half is not optional.** A client-side route only exists once 
the app is running, so a cold load, a refresh, a bookmark or any link from 
outside reaches Flask directly and 404s. A page with only the frontend half 
works for navigation that starts inside the app and nowhere else. The shell is 
served for sub-paths too, since the frontend matches a page's path as a prefix.
   
   Pages live in the existing view registry under a reserved location, 
inheriting its disposal, reactivity and error-boundary machinery rather than 
growing a second one beside it. `RouteSwitch` mounts them after Superset's own 
routes, so a page colliding with a host route is never reached and the host 
keeps its screen (with a warning logged). `isFrontendRoute` consults the 
registry live, since extensions register long after its route dictionary is 
built; `isHostRoute` is split out as the narrower question of whether the host 
owns a path.
   
   **Two decisions worth review:**
   
   *Access uses `@has_access` against a permission the page declares.* 
`flask_login.login_required` looks simpler and is not — 
`login_manager.login_view` is never set, so an unauthenticated request fails to 
build a redirect and the page answers 500 instead of sending the viewer to 
login. Declaring a permission is required rather than defaulting, because the 
insecure reading of an omitted field is the one nobody notices; a page names a 
resource its own API already registers, since a freshly invented permission is 
held by nobody until a role sync has run and the page would 403 for everyone 
including Admin.
   
   *Collisions are refused, not dropped*, each with a line naming both sides: 
two pages sharing an id would build one view class and the second would never 
reach the route table; two pages claiming one path would leave load order to 
decide which answered; a page claiming a path Superset already serves would 
shadow a real screen.
   
   Paths are normalised to one trailing slash and validated — a 
protocol-relative `//elsewhere/` would be an open redirect wherever the host 
emitted it as a link, `..` escapes the prefix the path exists to establish, `/` 
would be every page at once, and an empty or `.` segment is a path that is not 
the one it looks like. A page id is validated because it becomes part of a 
generated view class name, which Flask-AppBuilder also names the view's 
permissions after.
   
   **Two pre-existing registry defects are fixed here**, since contributed 
pages are the first case where two parties are likely to pick the same id: a 
second `registerView` under one id replaced the first silently, and the 
replaced registration's `Disposable` deleted by id unconditionally — so 
disposing it removed *the replacement*, meaning an extension tearing down its 
own view took another extension's with it.
   
   ### TESTING INSTRUCTIONS
   
   Automated:
   
   ```bash
   pytest tests/unit_tests/extensions/
   cd superset-extensions-cli && pytest tests/test_cli_build.py
   cd superset-frontend && npm run test -- src/core/views 
src/views/routes.test.tsx
   ```
   
   `tests/unit_tests/extensions/test_page_discovery.py` is the end-to-end one: 
it writes an extension to a temp directory, points `LOCAL_EXTENSIONS` at it, 
and asserts real discovery finds the declared pages, each becomes a route with 
its deep link, each renders the application shell with its bootstrap payload, 
and each stays guarded.
   
   Manual:
   
   1. Add a `contributions.pages` entry to a local extension's `extension.json` 
as above, with a `permission.resource` the extension's own API registers.
   2. In the extension's frontend entry, call `views.registerPage` with the 
same `path`.
   3. Build the extension and restart Superset.
   4. Navigate **directly** to the path (a cold load, not in-app navigation). 
The contributed page renders inside Superset's chrome. A sub-path renders it 
too. Logged out, you are redirected to login rather than given a 404 or a 500.
   5. The host logs the pairing at startup:
      `Serving extension page '<ext>:<page>' at '<path>', guarded by '<action> 
on <resource>'.`
   
   ### ADDITIONAL INFORMATION
   
   - [ ] Has associated issue:
   - [ ] Required feature flags:
   - [x] Changes UI
   - [ ] Includes DB Migration
   - [x] Introduces new feature or API
   - [ ] Removes existing feature or API
   
   Opened as a draft to start the conversation on the API shape before a SIP is 
written. The contribution schema (`contributions.pages`), 
`views.registerPage`/`getPages`, and the declared-permission contract are the 
parts that would need SIP sign-off.
   


-- 
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]

Reply via email to