mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-31 00:37:10 +02:00
chore: Add telemetry for user role provisioning (no changelog) (#22158)
This commit is contained in:
parent
6d281c68f3
commit
bfbbf016f3
|
|
@ -702,6 +702,38 @@ describe('TelemetryEventRelay', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('SSO events', () => {
|
||||
it('should track on `sso-user-project-access-updated` event', () => {
|
||||
const event: RelayEventMap['sso-user-project-access-updated'] = {
|
||||
userId: 'user123',
|
||||
projectsRemoved: 2,
|
||||
projectsAdded: 3,
|
||||
};
|
||||
|
||||
eventService.emit('sso-user-project-access-updated', event);
|
||||
|
||||
expect(telemetry.track).toHaveBeenCalledWith('Sso user project acess update', {
|
||||
user_id: 'user123',
|
||||
projects_removed: 2,
|
||||
projects_added: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('should track on `sso-user-instance-role-updated` event', () => {
|
||||
const event: RelayEventMap['sso-user-instance-role-updated'] = {
|
||||
userId: 'user123',
|
||||
role: 'global:admin',
|
||||
};
|
||||
|
||||
eventService.emit('sso-user-instance-role-updated', event);
|
||||
|
||||
expect(telemetry.track).toHaveBeenCalledWith('Sso user instance role update', {
|
||||
user_id: 'user123',
|
||||
role: 'global:admin',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('workflow events', () => {
|
||||
it('should track on `workflow-created` event', async () => {
|
||||
const event: RelayEventMap['workflow-created'] = {
|
||||
|
|
|
|||
|
|
@ -533,6 +533,19 @@ export type RelayEventMap = {
|
|||
|
||||
// #endregion
|
||||
|
||||
// #region SSO
|
||||
|
||||
'sso-user-project-access-updated': {
|
||||
projectsRemoved: number;
|
||||
projectsAdded: number;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
'sso-user-instance-role-updated': {
|
||||
role: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
// #region runner
|
||||
|
||||
'runner-task-requested': {
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ export class TelemetryEventRelay extends EventRelay {
|
|||
'ldap-settings-updated': (event) => this.ldapSettingsUpdated(event),
|
||||
'ldap-login-sync-failed': (event) => this.ldapLoginSyncFailed(event),
|
||||
'login-failed-due-to-ldap-disabled': (event) => this.loginFailedDueToLdapDisabled(event),
|
||||
'sso-user-project-access-updated': (event) => this.ssoUserProjectAccessUpdated(event),
|
||||
'sso-user-instance-role-updated': (event) => this.ssoUserInstanceRoleUpdated(event),
|
||||
'workflow-created': (event) => this.workflowCreated(event),
|
||||
'workflow-archived': (event) => this.workflowArchived(event),
|
||||
'workflow-unarchived': (event) => this.workflowUnarchived(event),
|
||||
|
|
@ -531,6 +533,29 @@ export class TelemetryEventRelay extends EventRelay {
|
|||
|
||||
// #endregion
|
||||
|
||||
// #region SSO
|
||||
|
||||
private ssoUserProjectAccessUpdated({
|
||||
projectsRemoved,
|
||||
projectsAdded,
|
||||
userId,
|
||||
}: RelayEventMap['sso-user-project-access-updated']) {
|
||||
this.telemetry.track('Sso user project acess update', {
|
||||
user_id: userId,
|
||||
projects_removed: projectsRemoved,
|
||||
projects_added: projectsAdded,
|
||||
});
|
||||
}
|
||||
|
||||
private ssoUserInstanceRoleUpdated({
|
||||
userId,
|
||||
role,
|
||||
}: RelayEventMap['sso-user-instance-role-updated']) {
|
||||
this.telemetry.track('Sso user instance role update', { user_id: userId, role });
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Workflow
|
||||
|
||||
private workflowCreated({
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { type Publisher } from '@/scaling/pubsub/publisher.service';
|
|||
import { type ProjectService } from '@/services/project.service.ee';
|
||||
import type { EntityManager } from '@n8n/typeorm';
|
||||
import { type InstanceSettings } from 'n8n-core';
|
||||
import { type EventService } from '@/events/event.service';
|
||||
|
||||
const globalConfig = mock<GlobalConfig>();
|
||||
const settingsRepository = mock<SettingsRepository>();
|
||||
|
|
@ -26,6 +27,7 @@ const userRepository = mock<UserRepository>();
|
|||
const entityManager = mock<EntityManager>();
|
||||
const projectRepository = mock<ProjectRepository>({ manager: entityManager });
|
||||
const projectService = mock<ProjectService>();
|
||||
const eventService = mock<EventService>();
|
||||
|
||||
const logger = mock<Logger>();
|
||||
const publisher = mock<Publisher>();
|
||||
|
|
@ -33,6 +35,7 @@ const roleRepository = mock<RoleRepository>();
|
|||
const instanceSettings = mock<InstanceSettings>();
|
||||
|
||||
const provisioningService = new ProvisioningService(
|
||||
eventService,
|
||||
globalConfig,
|
||||
settingsRepository,
|
||||
projectRepository,
|
||||
|
|
@ -282,6 +285,25 @@ describe('ProvisioningService', () => {
|
|||
{ userId: user.id, roleSlug: 'global:owner' },
|
||||
);
|
||||
});
|
||||
|
||||
it('sends telemetry event', async () => {
|
||||
const user = mock<User>({ id: 'user-123', role: { slug: 'global:member' } });
|
||||
const roleSlug = 'global:owner';
|
||||
|
||||
roleRepository.findOneOrFail.mockResolvedValue(
|
||||
mock<Role>({ slug: 'global:owner', roleType: 'global' }),
|
||||
);
|
||||
|
||||
provisioningService['isInstanceRoleProvisioningEnabled'] = jest.fn().mockResolvedValue(true);
|
||||
|
||||
await provisioningService.provisionInstanceRoleForUser(user, roleSlug);
|
||||
|
||||
expect(eventService.emit).toHaveBeenCalledTimes(1);
|
||||
expect(eventService.emit).toHaveBeenCalledWith('sso-user-instance-role-updated', {
|
||||
userId: user.id,
|
||||
role: roleSlug,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('provisionProjectRolesForUser', () => {
|
||||
|
|
@ -492,6 +514,33 @@ describe('ProvisioningService', () => {
|
|||
entityManager,
|
||||
);
|
||||
});
|
||||
|
||||
it('sends telemetry event', async () => {
|
||||
const userId = 'user-id-123';
|
||||
const projectIdToRole = ['project-1:viewer', 'project-2:editor'];
|
||||
projectRepository.find.mockResolvedValueOnce([
|
||||
mock<Project>({ id: 'project-1' }),
|
||||
mock<Project>({ id: 'project-2' }),
|
||||
]);
|
||||
projectRepository.find.mockResolvedValueOnce([
|
||||
mock<Project>({ id: 'project-3', type: 'team' }),
|
||||
]);
|
||||
roleRepository.find.mockResolvedValue([
|
||||
mock<Role>({ displayName: 'viewer', slug: 'project:viewer' }),
|
||||
mock<Role>({ displayName: 'editor', slug: 'project:editor' }),
|
||||
]);
|
||||
|
||||
provisioningService['isProjectRolesProvisioningEnabled'] = jest.fn().mockResolvedValue(true);
|
||||
|
||||
await provisioningService.provisionProjectRolesForUser(userId, projectIdToRole);
|
||||
|
||||
expect(eventService.emit).toHaveBeenCalledTimes(1);
|
||||
expect(eventService.emit).toHaveBeenCalledWith('sso-user-project-access-updated', {
|
||||
projectsAdded: 2,
|
||||
projectsRemoved: 1,
|
||||
userId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleReloadSsoProvisioningConfiguration', () => {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { jsonParse } from 'n8n-workflow';
|
|||
import { PROVISIONING_PREFERENCES_DB_KEY } from './constants';
|
||||
import { Not, In } from '@n8n/typeorm';
|
||||
import { OnPubSubEvent } from '@n8n/decorators';
|
||||
import { EventService } from '@/events/event.service';
|
||||
import { type Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { ZodError } from 'zod';
|
||||
|
|
@ -26,6 +27,7 @@ export class ProvisioningService {
|
|||
private provisioningConfig: ProvisioningConfigDto;
|
||||
|
||||
constructor(
|
||||
private readonly eventService: EventService,
|
||||
private readonly globalConfig: GlobalConfig,
|
||||
private readonly settingsRepository: SettingsRepository,
|
||||
private readonly projectRepository: ProjectRepository,
|
||||
|
|
@ -108,6 +110,11 @@ export class ProvisioningService {
|
|||
// No need to update record if the role hasn't changed
|
||||
if (user.role.slug !== dbRole.slug) {
|
||||
await this.userRepository.update(user.id, { role: { slug: dbRole.slug } });
|
||||
|
||||
this.eventService.emit('sso-user-instance-role-updated', {
|
||||
userId: user.id,
|
||||
role: dbRole.slug,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -242,6 +249,12 @@ export class ProvisioningService {
|
|||
await this.projectService.addUser(projectId, { userId, role: roleSlug }, tx);
|
||||
}
|
||||
});
|
||||
|
||||
this.eventService.emit('sso-user-project-access-updated', {
|
||||
projectsAdded: validProjectIds.size,
|
||||
projectsRemoved: projectsToRemoveAccessFrom.length,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
async patchConfig(rawConfig: unknown): Promise<ProvisioningConfigDto> {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const telemetry = useTelemetry();
|
|||
const toast = useToast();
|
||||
const message = useMessage();
|
||||
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||
const instanceId = useRootStore().instanceId;
|
||||
|
||||
const savingForm = ref<boolean>(false);
|
||||
|
||||
|
|
@ -151,6 +152,7 @@ async function onOidcSettingsSave(provisioningChangesConfirmed: boolean = false)
|
|||
|
||||
if (isUserRoleProvisioningChanged()) {
|
||||
await saveProvisioningConfig();
|
||||
sendTrackingEventForUserProvisioning();
|
||||
showUserRoleProvisioningDialog.value = false;
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +173,7 @@ async function onOidcSettingsSave(provisioningChangesConfirmed: boolean = false)
|
|||
|
||||
function sendTrackingEvent(config: OidcConfigDto) {
|
||||
const trackingMetadata = {
|
||||
instance_id: useRootStore().instanceId,
|
||||
instance_id: instanceId,
|
||||
authentication_method: SupportedProtocols.OIDC,
|
||||
discovery_endpoint: config.discoveryEndpoint,
|
||||
is_active: config.loginEnabled,
|
||||
|
|
@ -179,6 +181,14 @@ function sendTrackingEvent(config: OidcConfigDto) {
|
|||
telemetry.track('User updated single sign on settings', trackingMetadata);
|
||||
}
|
||||
|
||||
function sendTrackingEventForUserProvisioning() {
|
||||
telemetry.track('User updated provisioning settings', {
|
||||
instance_id: instanceId,
|
||||
authentication_method: SupportedProtocols.OIDC,
|
||||
updated_setting: userRoleProvisioning.value,
|
||||
});
|
||||
}
|
||||
|
||||
const goToUpgrade = () => {
|
||||
void pageRedirectionHelper.goToUpgrade('sso', 'upgrade-sso');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const telemetry = useTelemetry();
|
|||
const toast = useToast();
|
||||
const message = useMessage();
|
||||
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||
const instanceId = useRootStore().instanceId;
|
||||
|
||||
const savingForm = ref<boolean>(false);
|
||||
|
||||
|
|
@ -122,7 +123,7 @@ const sendTrackingEvent = (config?: SamlPreferences) => {
|
|||
return;
|
||||
}
|
||||
const trackingMetadata = {
|
||||
instance_id: useRootStore().instanceId,
|
||||
instance_id: instanceId,
|
||||
authentication_method: SupportedProtocols.SAML,
|
||||
identity_provider: config.metadataUrl ? 'metadata' : 'xml',
|
||||
is_active: config.loginEnabled ?? false,
|
||||
|
|
@ -130,6 +131,14 @@ const sendTrackingEvent = (config?: SamlPreferences) => {
|
|||
telemetry.track('User updated single sign on settings', trackingMetadata);
|
||||
};
|
||||
|
||||
const sendTrackingEventForUserProvisioning = () => {
|
||||
telemetry.track('User updated provisioning settings', {
|
||||
instance_id: instanceId,
|
||||
authentication_method: SupportedProtocols.SAML,
|
||||
updated_setting: userRoleProvisioning.value,
|
||||
});
|
||||
};
|
||||
|
||||
const promptConfirmDisablingSamlLogin = async () => {
|
||||
const confirmAction = await message.confirm(
|
||||
i18n.baseText('settings.sso.confirmMessage.beforeSaveForm.message', {
|
||||
|
|
@ -221,6 +230,7 @@ const onSave = async (provisioningChangesConfirmed: boolean = false) => {
|
|||
|
||||
if (isUserRoleProvisioningChanged()) {
|
||||
await saveProvisioningConfig();
|
||||
sendTrackingEventForUserProvisioning();
|
||||
showUserRoleProvisioningDialog.value = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user