Skip to content

Commit

Permalink
Merge pull request #77 from Tietokilta/feature/audit-logging
Browse files Browse the repository at this point in the history
Add audit logging
  • Loading branch information
PurkkaKoodari committed May 23, 2022
2 parents a4162b1 + 4bb5fcc commit bfaae44
Show file tree
Hide file tree
Showing 45 changed files with 927 additions and 59 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ DELETION_GRACE_PERIOD_DAYS=14
# Whether or not new admin accounts can be added. REMEMBER TO DISABLE AFTER SETUP.
ADMIN_REGISTRATION_ALLOWED=true

# Whether or not to trust X-Forwarded-For headers for remote IP. Set to true IF
# AND ONLY IF running behind a proxy that sets this header.
TRUST_PROXY=false


# Authentication secrets

Expand Down
14 changes: 13 additions & 1 deletion docs/migration.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Migration
# Migration

This file documents the migration process from the Athene-created version of Ilmomasiina to the new one.

Expand Down Expand Up @@ -103,4 +103,16 @@ ADD `namePublic` BOOLEAN NOT NULL DEFAULT 0 AFTER `lastName`;
-- keep names public in previously existing signups
UPDATE `signup`
SET `namePublic` = 1;

-- add audit log
CREATE TABLE `auditlog` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`user` VARCHAR(255) DEFAULT NULL,
`ipAddress` VARCHAR(64) NOT NULL,
`action` VARCHAR(32) NOT NULL,
`details` TEXT NOT NULL DEFAULT '',
`createdAt` DATETIME NOT NULL,
`updatedAt` DATETIME NOT NULL,
PRIMARY KEY (`id`)
);
```
12 changes: 12 additions & 0 deletions packages/ilmomasiina-backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,28 @@ import cron from 'node-cron';

import config from './config';
import anonymizeOldSignups from './cron/anonymizeOldSignups';
import deleteOldAuditLogs from './cron/deleteOldAuditLogs';
import deleteUnconfirmedSignups from './cron/deleteUnconfirmedSignups';
import removeDeletedData from './cron/removeDeletedData';
import setupDatabase from './models';
import services from './services';
import { remoteIp } from './util/auditLog';

export default async function initApp() {
await setupDatabase();

const app = express(feathers());

// Get IPs from X-Forwarded-For
if (config.isAzure || config.trustProxy) {
app.set('trust proxy', true);
}

app
.use(compress())
.use(json())
.use(urlencoded({ extended: true }))
.use(remoteIp)
.configure(rest())
.configure(services);

Expand Down Expand Up @@ -70,5 +79,8 @@ export default async function initApp() {
// Daily at 8am, delete deleted items from the database
cron.schedule('0 8 * * *', removeDeletedData);

// Daily at 8am, delete old audit logs
cron.schedule('0 8 * * *', deleteOldAuditLogs);

return app;
}
1 change: 1 addition & 0 deletions packages/ilmomasiina-backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const config = <const>{
isAzure: process.env.WEBSITE_SITE_NAME !== undefined,

enforceHttps: envBoolean('ENFORCE_HTTPS', false),
trustProxy: envBoolean('TRUST_PROXY', false),
frontendFilesPath: frontendFilesPath(),

clearDbUrl: envString('CLEARDB_DATABASE_URL', null),
Expand Down
15 changes: 15 additions & 0 deletions packages/ilmomasiina-backend/src/cron/deleteOldAuditLogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import moment from 'moment';
import { Op, WhereOptions } from 'sequelize';

import config from '../config';
import { AuditLog } from '../models/auditlog';

export default async function deleteOldAuditLogs() {
await AuditLog.unscoped().destroy({
where: {
createdAt: {
[Op.lt]: moment().subtract(config.anonymizeAfterDays, 'days').toDate(),
},
} as WhereOptions,
});
}
74 changes: 74 additions & 0 deletions packages/ilmomasiina-backend/src/models/auditlog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
DataTypes, Model, Optional, Sequelize,
} from 'sequelize';

import AuditLogAttributes from '@tietokilta/ilmomasiina-models/src/models/auditlog';
import { RANDOM_ID_LENGTH } from './randomId';

export interface AuditLogCreationAttributes extends Optional<AuditLogAttributes, 'id'> {}

export class AuditLog extends Model<AuditLogAttributes, AuditLogCreationAttributes> implements AuditLogAttributes {
public id!: number;
public user!: string | null;
public ipAddress!: string;
public action!: string;
public eventId!: string | null;
public eventName!: string | null;
public signupId!: string | null;
public signupName!: string | null;
public extra!: string;

public readonly createdAt!: Date;
public readonly updatedAt!: Date;
}

export default function setupAuditLogModel(sequelize: Sequelize) {
AuditLog.init(
{
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true,
},
user: {
type: DataTypes.STRING,
allowNull: true,
},
ipAddress: {
type: DataTypes.STRING(64),
allowNull: false,
},
action: {
type: DataTypes.STRING(32),
allowNull: false,
},
eventId: {
type: DataTypes.CHAR(RANDOM_ID_LENGTH),
allowNull: true,
},
eventName: {
type: DataTypes.STRING,
allowNull: true,
},
signupId: {
type: DataTypes.CHAR(RANDOM_ID_LENGTH),
allowNull: true,
},
signupName: {
type: DataTypes.STRING,
allowNull: true,
},
extra: {
type: DataTypes.TEXT,
allowNull: true,
},
},
{
sequelize,
modelName: 'auditlog',
freezeTableName: true,
},
);

return AuditLog;
}
2 changes: 2 additions & 0 deletions packages/ilmomasiina-backend/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Sequelize } from 'sequelize';
import { SequelizeStorage, Umzug } from 'umzug';

import setupAnswerModel, { Answer } from './answer';
import setupAuditLogModel from './auditlog';
import sequelizeConfig from './config';
import setupEventModel, { Event } from './event';
import migrations from './migrations';
Expand Down Expand Up @@ -45,6 +46,7 @@ export default async function setupDatabase() {
setupQuestionModel(sequelize);
setupAnswerModel(sequelize);
setupUserModel(sequelize);
setupAuditLogModel(sequelize);

Event.hasMany(Question, {
foreignKey: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { DataTypes, Sequelize } from 'sequelize';
import { RunnableMigration } from 'umzug';

// Constant from ../randomId
const RANDOM_ID_LENGTH = 12;

const migration: RunnableMigration<Sequelize> = {
name: '0001-add-audit-logs',
async up({ context: sequelize }) {
const query = sequelize.getQueryInterface();
await query.createTable(
'auditlog',
{
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true,
},
user: {
type: DataTypes.STRING,
allowNull: true,
},
ipAddress: {
type: DataTypes.STRING(64),
allowNull: false,
},
action: {
type: DataTypes.STRING(32),
allowNull: false,
},
eventId: {
type: DataTypes.CHAR(RANDOM_ID_LENGTH),
allowNull: true,
},
eventName: {
type: DataTypes.STRING,
allowNull: true,
},
signupId: {
type: DataTypes.CHAR(RANDOM_ID_LENGTH),
allowNull: true,
},
signupName: {
type: DataTypes.STRING,
allowNull: true,
},
extra: {
type: DataTypes.TEXT,
allowNull: true,
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
},
},
);
},
};

export default migration;
2 changes: 2 additions & 0 deletions packages/ilmomasiina-backend/src/models/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { Sequelize } from 'sequelize';
import { RunnableMigration } from 'umzug';

import _0000_initial from './0000-initial';
import _0001_add_audit_logs from './0001-add-audit-logs';

const migrations: RunnableMigration<Sequelize>[] = [
_0000_initial,
_0001_add_audit_logs,
];

export default migrations;
3 changes: 3 additions & 0 deletions packages/ilmomasiina-backend/src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export default function setupUserModel(sequelize: Sequelize) {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true,
},
},
password: {
type: DataTypes.STRING,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Params } from '@feathersjs/feathers';
import _ from 'lodash';

import {
Expand All @@ -10,9 +11,10 @@ import { AdminEventGetResponse } from '@tietokilta/ilmomasiina-models/src/servic
import { Event } from '../../../models/event';
import { Question } from '../../../models/question';
import { Quota } from '../../../models/quota';
import { logEvent } from '../../../util/auditLog';
import { getEventDetailsForAdmin } from '../../event/getEventDetails';

export default async (data: AdminEventCreateBody): Promise<AdminEventGetResponse> => {
export default async (data: AdminEventCreateBody, params: Params | undefined): Promise<AdminEventGetResponse> => {
// Pick only allowed attributes and add order
const attribs = {
..._.pick(data, adminEventCreateEventAttrs),
Expand All @@ -27,8 +29,8 @@ export default async (data: AdminEventCreateBody): Promise<AdminEventGetResponse
};

// Create the event with relations - Sequelize will handle validation
const event = await Event.sequelize!.transaction((transaction) => (
Event.create(attribs, {
const event = await Event.sequelize!.transaction(async (transaction) => {
const created = await Event.create(attribs, {
transaction,
include: [
{
Expand All @@ -40,8 +42,12 @@ export default async (data: AdminEventCreateBody): Promise<AdminEventGetResponse
required: false,
},
],
})
));
});

await logEvent('event.create', { event: created, params, transaction });

return created;
});

return getEventDetailsForAdmin(event.id);
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { NotFound } from '@feathersjs/errors';
import { Params } from '@feathersjs/feathers';

import { Event } from '../../../models/event';
import { logEvent } from '../../../util/auditLog';

export default async (id: Event['id']): Promise<null> => {
export default async (id: Event['id'], params: Params | undefined): Promise<null> => {
const event = await Event.findByPk(id);
if (event === null) {
throw new NotFound('No event found with id');
Expand All @@ -11,5 +13,9 @@ export default async (id: Event['id']): Promise<null> => {
// Delete the DB object
await event?.destroy();

if (event) {
await logEvent('event.delete', { params, event });
}

return null;
};
12 changes: 6 additions & 6 deletions packages/ilmomasiina-backend/src/services/admin/event/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ export const adminEventService: Partial<ServiceMethods<AdminEventsServiceTypes>>
return getEventDetailsForAdmin(String(id));
},

create(data: AdminEventCreateBody) {
return createEvent(data);
create(data: AdminEventCreateBody, params) {
return createEvent(data, params);
},

patch(id, data: Partial<AdminEventUpdateBody>) {
return updateEvent(String(id), data);
patch(id, data: Partial<AdminEventUpdateBody>, params) {
return updateEvent(String(id), data, params);
},

remove(id) {
return deleteEvent(String(id));
remove(id, params) {
return deleteEvent(String(id), params);
},
};

Expand Down
Loading

0 comments on commit bfaae44

Please sign in to comment.