Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(audit-logs): migrate audit logs to v5 #20251

Merged
merged 17 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/core/admin/ee/server/src/audit-logs/models/audit-log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Model } from '@strapi/database';

const auditLog: Model = {
uid: 'admin::audit-log',
tableName: 'strapi_audit_logs',
singularName: 'audit-log',
attributes: {
id: {
type: 'increments',
},
action: {
type: 'string',
required: true,
},
date: {
type: 'datetime',
required: true,
},
// @ts-expect-error database model is not yet updated to support useJoinTable
user: {
type: 'relation',
relation: 'oneToOne',
target: 'admin::user',
useJoinTable: false,
},
payload: {
type: 'json',
},
},
};

export { auditLog };
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { enableFeatureMiddleware } from './utils';
import { enableFeatureMiddleware } from '../../routes/utils';

export default {
type: 'admin',
routes: [
{
method: 'GET',
path: '/audit-logs',
handler: 'auditLogs.findMany',
handler: 'audit-logs.findMany',
config: {
middlewares: [enableFeatureMiddleware('audit-logs')],
policies: [
Expand All @@ -23,7 +23,7 @@ export default {
{
method: 'GET',
path: '/audit-logs/:id',
handler: 'auditLogs.findOne',
handler: 'audit-logs.findOne',
config: {
middlewares: [enableFeatureMiddleware('audit-logs')],
policies: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { Core } from '@strapi/types';

interface Event {
action: string;
date: Date;
userId: string | number;
payload: Record<string, unknown>;
}

interface Log extends Omit<Event, 'userId'> {
user: string | number;
}

const getSanitizedUser = (user: any) => {
let displayName = user.email;

if (user.username) {
displayName = user.username;
} else if (user.firstname && user.lastname) {
displayName = `${user.firstname} ${user.lastname}`;
}

return {
id: user.id,
email: user.email,
displayName,
};
};

/**
* @description
* Manages audit logs interaction with the database. Accessible via strapi.get('audit-logs')
*/
const createAuditLogsService = (strapi: Core.Strapi) => {
return {
async saveEvent(event: Event) {
const { userId, ...rest } = event;

const auditLog: Log = { ...rest, user: userId };

// Save to database
await strapi.db?.query('admin::audit-log').create({ data: auditLog });

return this;
},

async findMany(query: unknown) {
const { results, pagination } = await strapi.db?.query('admin::audit-log').findPage({
populate: ['user'],
select: ['action', 'date', 'payload'],
...strapi.get('query-params').transform('admin::audit-log', query),
});

const sanitizedResults = results.map((result: any) => {
const { user, ...rest } = result;
return {
...rest,
user: user ? getSanitizedUser(user) : null,
};
});

return {
results: sanitizedResults,
pagination,
};
},

async findOne(id: unknown) {
const result: any = await strapi.db?.query('admin::audit-log').findOne({
where: { id },
populate: ['user'],
select: ['action', 'date', 'payload'],
});

if (!result) {
return null;
}

const { user, ...rest } = result;
return {
...rest,
user: user ? getSanitizedUser(user) : null,
};
},

deleteExpiredEvents(expirationDate: Date) {
return strapi.db?.query('admin::audit-log').deleteMany({
where: {
date: {
$lt: expirationDate.toISOString(),
},
},
});
},
};
};

export { createAuditLogsService };
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Core } from '@strapi/types';
import localProvider from '@strapi/provider-audit-logs-local';
import { auditLog } from '../models/audit-log';
import { scheduleJob } from 'node-schedule';

const DEFAULT_RETENTION_DAYS = 90;
Expand Down Expand Up @@ -35,22 +35,6 @@ const defaultEvents = [
'permission.delete',
];

const getSanitizedUser = (user: any) => {
let displayName = user.email;

if (user.username) {
displayName = user.username;
} else if (user.firstname && user.lastname) {
displayName = `${user.firstname} ${user.lastname}`;
}

return {
id: user.id,
email: user.email,
displayName,
};
};

const getEventMap = (defaultEvents: any) => {
const getDefaultPayload = (...args: any) => args[0];

Expand All @@ -62,7 +46,9 @@ const getEventMap = (defaultEvents: any) => {
};

const getRetentionDays = (strapi: Core.Strapi) => {
const licenseRetentionDays = strapi.ee.features.get('audit-logs')?.options.retentionDays;
const featureConfig = strapi.ee.features.get('audit-logs');
const licenseRetentionDays =
typeof featureConfig === 'object' && featureConfig?.options.retentionDays;
const userRetentionDays = strapi.config.get('admin.auditLogs.retentionDays');

// For enterprise plans, use 90 days by default, but allow users to override it
Expand All @@ -79,9 +65,14 @@ const getRetentionDays = (strapi: Core.Strapi) => {
return licenseRetentionDays;
};

const createAuditLogsService = (strapi: Core.Strapi) => {
/**
* @description
* Manages the the lifecycle of audit logs. Accessible via strapi.get('audit-logs-lifecycles')
*/
const createAuditLogsLifecycle = (strapi: Core.Strapi) => {
markkaylor marked this conversation as resolved.
Show resolved Hide resolved
// Manage internal service state privately
const state = {} as any;
const auditLogsService = strapi.get('audit-logs');

// NOTE: providers should be able to replace getEventMap to add or remove events
const eventMap = getEventMap(defaultEvents);
Expand All @@ -90,9 +81,8 @@ const createAuditLogsService = (strapi: Core.Strapi) => {
const requestState = strapi.requestContext.get()?.state;

// Ignore events with auth strategies different from admin
const isUsingAdminAuth = requestState?.auth?.strategy.name === 'admin';
const isUsingAdminAuth = requestState?.route.info.type === 'admin';
const user = requestState?.user;

if (!isUsingAdminAuth || !user) {
return null;
}
Expand All @@ -105,6 +95,7 @@ const createAuditLogsService = (strapi: Core.Strapi) => {
}

// Ignore some events based on payload
// TODO: What does this ignore in upload? Why would we want to ignore anything?
const ignoredUids = ['plugin::upload.file', 'plugin::upload.folder'];
if (ignoredUids.includes(args[0]?.uid)) {
Comment on lines 98 to 99
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Feranchz do you have any idea what this about? As far as I can tell there isn't anything to ignore from the upload plugin 🤔.

return null;
Expand All @@ -118,17 +109,17 @@ const createAuditLogsService = (strapi: Core.Strapi) => {
};
};

async function handleEvent(name: string, ...args: any) {
const handleEvent = async (name: string, ...args: any) => {
const processedEvent = processEvent(name, ...args);

if (processedEvent) {
// This stores the event when after the transaction is committed,
// so it's not stored if the transaction is rolled back
await strapi.db.transaction(({ onCommit }) => {
onCommit(() => state.provider.saveEvent(processedEvent));
onCommit(() => auditLogsService.saveEvent(processedEvent));
});
}
}
};

return {
async register() {
Expand Down Expand Up @@ -160,58 +151,24 @@ const createAuditLogsService = (strapi: Core.Strapi) => {
this.destroy();
});

// Register the provider now because collections can't be added later at runtime
state.provider = await localProvider.register({ strapi });

// Check current state of license
if (!strapi.ee.features.isEnabled('audit-logs')) {
return this;
}

// Start saving events
state.eventHubUnsubscribe = strapi.eventHub.subscribe(handleEvent.bind(this));
state.eventHubUnsubscribe = strapi.eventHub.subscribe(handleEvent);

// Manage audit logs auto deletion
const retentionDays = getRetentionDays(strapi);
state.deleteExpiredJob = scheduleJob('0 0 * * *', () => {
const expirationDate = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
state.provider.deleteExpiredEvents(expirationDate);
auditLogsService.deleteExpiredEvents(expirationDate);
});

return this;
},

async findMany(query: unknown) {
const { results, pagination } = await state.provider.findMany(query);

const sanitizedResults = results.map((result: any) => {
const { user, ...rest } = result;
return {
...rest,
user: user ? getSanitizedUser(user) : null,
};
});

return {
results: sanitizedResults,
pagination,
};
},

async findOne(id: unknown) {
const result = await state.provider.findOne(id);

if (!result) {
return null;
}

const { user, ...rest } = result;
return {
...rest,
user: user ? getSanitizedUser(user) : null,
};
},

unsubscribe() {
if (state.eeDisableUnsubscribe) {
state.eeDisableUnsubscribe();
Expand All @@ -234,4 +191,4 @@ const createAuditLogsService = (strapi: Core.Strapi) => {
};
};

export default createAuditLogsService;
export { createAuditLogsLifecycle };
2 changes: 0 additions & 2 deletions packages/core/admin/ee/server/src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import type {} from 'koa-body';
import authentication from './authentication';
import role from './role';
import user from './user';
import auditLogs from './audit-logs';
import admin from './admin';

export default {
authentication,
role,
user,
auditLogs,
markkaylor marked this conversation as resolved.
Show resolved Hide resolved
admin,
};
4 changes: 0 additions & 4 deletions packages/core/admin/ee/server/src/destroy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,5 @@ import type { Core } from '@strapi/types';
import executeCEDestroy from '../../../server/src/destroy';

export default async ({ strapi }: { strapi: Core.Strapi }) => {
if (strapi.ee.features.isEnabled('audit-logs')) {
strapi.get('audit-logs').destroy();
}

await executeCEDestroy();
};