feat(core): Add project-scoped insights permissions#28331
feat(core): Add project-scoped insights permissions#28331
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cope Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Bundle ReportChanges will increase total bundle size by 72 bytes (0.0%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: editor-ui-esmAssets Changed:
|
| 'projectVariable:create', | ||
| 'projectVariable:update', | ||
| 'projectVariable:delete', | ||
| 'insights:list', |
There was a problem hiding this comment.
All of these permission changes look good. 👍
|
|
||
| import type { InsightsService } from './insights.service'; | ||
|
|
||
| export abstract class InsightsBaseController { |
There was a problem hiding this comment.
While it does make some sense, no other controller uses this inheritance pattern.
Most of them use a "dependency injection" (DI) pattern.
As we've also got similar logic in the public API, it would make sense to pull these functions with the business logic into a small service which could be injected into the controller and also used by the public API (which can't inherit from this controller). It'll avoid us having duplicated logic.
There was a problem hiding this comment.
yeah it is literally just for not duplicating the logic (code). even though it is only a couple of lines. i saw that we don't do this anywhere else. but i thought it might be a better pattern?
There was a problem hiding this comment.
The compositional pattern is usually better than inheritance as it's generally easier to test and has less coupling.
There was a problem hiding this comment.
I think the only reason we've gone for the inheritance model here is because the insightsService is in use. I agree with @MarcL RE the inheritance vs composition 👍 - we need a "good reason" to reach for inheritance usually and I don't think this is a "good reason"...
Other than the checkDatesFiltersAgainstLicense these can be functional "utility" functions.
(being picky) I also think theres a bit of a "smell" in here 👀 .. prepareDateFilters is doing more than it suggests:
- Validating dates and throwing an error
- Extracting and defaulting dates (now - 7d) - I think the defaulting should happen higher up when you pluck the values out of the request payload as it's "specific" to each call.
- Checking licensing information
| }); | ||
|
|
||
| describe('InsightsProjectController', () => { | ||
| const insightsByPeriodRepository = mockInstance(InsightsByPeriodRepository); |
There was a problem hiding this comment.
The controller has the InsightsService injected into it but we're mocking the InsightsByPeriodRepository (which makes the DB calls). This is coupling the tests to the internals. If they change then the tests could break even though we've not changed the controller. Probably better to mock the InsightsService instead.
| import { InsightsBaseController } from './insights-base.controller'; | ||
| import { InsightsService } from './insights.service'; | ||
|
|
||
| @RestController('/insights/projects') |
There was a problem hiding this comment.
We already have some API endpoints that exist on the /projects endpoint rather than this /insights endpoint. The pattern is /projects/:projectId/{resouce} e.g. /projects/:projectId/folders and /projects/:projectId/data-tables. It's worth following that pattern for consistency with other project endpoints.
There was a problem hiding this comment.
We also don't check the validity of the projectId being passed in. The folder controller has a middleware pattern which returns a 404 if no project exists. We should consider doing something similar (or refactor that middleware out to a shared function if that makes sense).
See:
n8n/packages/cli/src/controllers/folder.controller.ts
Lines 42 to 56 in 9072365
| _req: AuthenticatedRequest, | ||
| _res: Response, | ||
| @Param('projectId') projectId: string, | ||
| @Query query: InsightsDateFilterDto = {}, |
There was a problem hiding this comment.
The InsightsDateFilterDto for the incoming request query parameters contains an optional projectId parameter so this means we have 2 ways of passing in the projectId. I think the controller projectId in the path may take precedence but we should perhaps use another DTO here?
There was a problem hiding this comment.
yes that's correct. i created a test for that actually. but yeah maybe making this explicit is better
geemanjs
left a comment
There was a problem hiding this comment.
Given the historic problems in insights - i'd suggest also writing an integration test around these additions.
| projectId, | ||
| startDate, | ||
| endDate, | ||
| })) as InsightsByTime[]; |
There was a problem hiding this comment.
If the response contains more/different fields than user expects we should pick out the expected fields rather than cast the type.
Here is another way:
https://www.notion.so/n8n/Request-Response-2265b6e0c94f8032a12ce711f5478a60?source=copy_link#2265b6e0c94f804581f5c4d68e9d5773
| projectId, | ||
| startDate, | ||
| endDate, | ||
| })) as RestrictedInsightsByTime[]; |
There was a problem hiding this comment.
Same here - The last thing we want is to send "data" back that we don't want in the response.
| private checkDatesFiltersAgainstLicense(dateFilters: { startDate: Date; endDate: Date }) { | ||
| try { | ||
| this.insightsService.validateDateFiltersLicense(dateFilters); | ||
| } catch (error: unknown) { | ||
| if (error instanceof UserError) { | ||
| throw new ForbiddenError(error.message); | ||
| } | ||
|
|
||
| throw new InternalServerError(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Any reason why we map these errors? The ones coming back from the insight service seem to give the right information
|
|
||
| import type { InsightsService } from './insights.service'; | ||
|
|
||
| export abstract class InsightsBaseController { |
There was a problem hiding this comment.
I think the only reason we've gone for the inheritance model here is because the insightsService is in use. I agree with @MarcL RE the inheritance vs composition 👍 - we need a "good reason" to reach for inheritance usually and I don't think this is a "good reason"...
Other than the checkDatesFiltersAgainstLicense these can be functional "utility" functions.
(being picky) I also think theres a bit of a "smell" in here 👀 .. prepareDateFilters is doing more than it suggests:
- Validating dates and throwing an error
- Extracting and defaulting dates (now - 7d) - I think the defaulting should happen higher up when you pluck the values out of the request payload as it's "specific" to each call.
- Checking licensing information
| const today = new Date(); | ||
|
|
||
| if (!query.startDate) { | ||
| return { | ||
| startDate: DateTime.now().minus({ days: 7 }).toJSDate(), | ||
| endDate: today, | ||
| }; | ||
| } |
There was a problem hiding this comment.
We should use DateTime.utc() for all of these or we'll be using the "system time zone" which could lead to mistakes.
Summary
Splits insights permissions from global-only to global + project-scoped, following the external secrets pattern.
Changes:
insights:listscope to all 4 project roles (admin, personal owner, editor, viewer)InsightsControllerintoInsightsBaseControllerInsightsProjectControllerat/insights/projects/:projectId/*with@ProjectScope('insights:list')/insights/*with@GlobalScope) is unchangedHow it works:
/insights/*for instance-wide data (unchanged)/insights/projects/:projectId/*for project-scoped data@ProjectScopemiddleware handles authorization automatically via route paramsNo frontend changes — the UI continues using the existing global endpoints. Frontend integration (KPI cards, project filtering, dashboard) comes in a follow-up PR (LIGO-358).
How to test:
Related Linear tickets, Github issues, and Community forum posts
https://linear.app/n8n/issue/LIGO-358
Review / Merge checklist
release/backport(if the PR is an urgent fix that needs to be backported)