Skip to content

feat(core): Add project-scoped insights permissions#28331

Draft
fabian8n wants to merge 7 commits intomasterfrom
insights-permissions-split
Draft

feat(core): Add project-scoped insights permissions#28331
fabian8n wants to merge 7 commits intomasterfrom
insights-permissions-split

Conversation

@fabian8n
Copy link
Copy Markdown

@fabian8n fabian8n commented Apr 10, 2026

  • I have seen this code, I have run this code, and I take responsibility for this code.

Summary

Splits insights permissions from global-only to global + project-scoped, following the external secrets pattern.

Changes:

  • Adds insights:list scope to all 4 project roles (admin, personal owner, editor, viewer)
  • Extracts shared date filter logic from InsightsController into InsightsBaseController
  • Creates new InsightsProjectController at /insights/projects/:projectId/* with @ProjectScope('insights:list')
  • Registers the new controller in the insights module
  • Existing global controller (/insights/* with @GlobalScope) is unchanged

How it works:

  • Global owners/admins use /insights/* for instance-wide data (unchanged)
  • Project members use /insights/projects/:projectId/* for project-scoped data
  • @ProjectScope middleware handles authorization automatically via route params
  • A user with access to multiple projects can query each independently

No 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:

  • Existing insights tests pass (no regression)
  • New project controller tests verify projectId routing, date validation, license checks, and route param vs query param precedence

Related Linear tickets, Github issues, and Community forum posts

https://linear.app/n8n/issue/LIGO-358

Review / Merge checklist

  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with release/backport (if the PR is an urgent fix that needs to be backported)

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 10, 2026

Codecov Report

❌ Patch coverage is 98.24561% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...i/src/modules/insights/insights-base.controller.ts 96.87% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 10, 2026

Bundle Report

Changes will increase total bundle size by 72 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
editor-ui-esm 45.57MB 72 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: editor-ui-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/worker-*.js 3.14MB 3.16MB 17560.39% ⚠️
assets/worker-*.js -3.14MB 17.9kB -99.43%
assets/constants-*.js 72 bytes 3.14MB 0.0%

@fabian8n fabian8n requested review from MarcL and geemanjs April 10, 2026 13:47
@n8n-assistant n8n-assistant Bot added core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team labels Apr 10, 2026
'projectVariable:create',
'projectVariable:update',
'projectVariable:delete',
'insights:list',
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.

All of these permission changes look good. 👍


import type { InsightsService } from './insights.service';

export abstract class InsightsBaseController {
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.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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?

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.

The compositional pattern is usually better than inheritance as it's generally easier to test and has less coupling.

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

  1. Validating dates and throwing an error
  2. 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.
  3. Checking licensing information

});

describe('InsightsProjectController', () => {
const insightsByPeriodRepository = mockInstance(InsightsByPeriodRepository);
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.

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')
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.

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.

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.

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:

@Middleware()
async validateProjectExists(
req: AuthenticatedRequest<{ projectId: string }>,
res: Response,
next: NextFunction,
) {
try {
const { projectId } = req.params;
await this.projectService.getProject(projectId);
next();
} catch (e) {
res.status(404).send('Project not found');
return;
}
}

_req: AuthenticatedRequest,
_res: Response,
@Param('projectId') projectId: string,
@Query query: InsightsDateFilterDto = {},
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.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

yes that's correct. i created a test for that actually. but yeah maybe making this explicit is better

Copy link
Copy Markdown
Contributor

@geemanjs geemanjs left a comment

Choose a reason for hiding this comment

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

Given the historic problems in insights - i'd suggest also writing an integration test around these additions.

projectId,
startDate,
endDate,
})) as InsightsByTime[];
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.

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[];
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.

Same here - The last thing we want is to send "data" back that we don't want in the response.

Comment on lines +70 to +80
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();
}
}
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.

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 {
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 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:

  1. Validating dates and throwing an error
  2. 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.
  3. Checking licensing information

Comment on lines +58 to +65
const today = new Date();

if (!query.startDate) {
return {
startDate: DateTime.now().minus({ days: 7 }).toJSDate(),
endDate: today,
};
}
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.

We should use DateTime.utc() for all of these or we'll be using the "system time zone" which could lead to mistakes.

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

Labels

core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants