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

Audit logs for saas app + admin portal #1972

Closed
wants to merge 39 commits into from
Closed
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
60cd162
Store product config
devkiran Nov 17, 2023
cf9a051
Handle Product not found.
devkiran Nov 20, 2023
7cae843
Product id is required
devkiran Nov 20, 2023
fc0a32d
Cleanup
devkiran Nov 20, 2023
8be2e20
Merge branch 'main' into feat/product-config
devkiran Nov 21, 2023
f23eb97
Refactor API route validation in middleware.ts
devkiran Nov 21, 2023
837be17
Show friendly product name instead of id
devkiran Nov 23, 2023
eda43bb
Refactor product fetching
devkiran Nov 23, 2023
add5398
Merge branch 'main' into feat/product-config
Nov 23, 2023
533c744
Audit Logs for Jackson + SaaS App
devkiran Nov 23, 2023
1867c6e
Cleanup type
devkiran Nov 23, 2023
aa9562a
Add description to reportEvent function
devkiran Nov 24, 2023
85050d5
Remove unused import
devkiran Nov 24, 2023
141d91d
Merge branch 'main' into feat/audit-logs-saas-app
Nov 24, 2023
a06c76a
Fix error handling in reportEvent function
devkiran Nov 24, 2023
ae1ee83
Merge branch 'feat/audit-logs-saas-app' of https://github.com/boxyhq/…
devkiran Nov 24, 2023
303dc81
Merge branch 'main' into feat/audit-logs-saas-app
Nov 27, 2023
e086711
Add target and tenant fields
devkiran Nov 27, 2023
681cf95
Error handling
devkiran Nov 27, 2023
d2cd190
Event reporting for
devkiran Nov 28, 2023
f3bdf88
Merge branch 'main' into feat/audit-logs-saas-app
Nov 28, 2023
70d7a21
Merge branch 'feat/audit-logs-saas-app' of https://github.com/boxyhq/…
devkiran Nov 28, 2023
0097cfe
Add retraced event to dsync
devkiran Nov 28, 2023
1a62364
Add retraced event tracking for setup link
devkiran Nov 28, 2023
6228d71
Add retraced event logging for federated SAML API
devkiran Nov 28, 2023
b88ca2e
Fix track events for API events
devkiran Nov 28, 2023
e498c9a
Add branding update event to retraced
devkiran Nov 28, 2023
5d1c5cd
Fix optional parameters in reportAdminPortalEvent
devkiran Nov 28, 2023
116ad0d
Fix import statement and update event CRUD
devkiran Nov 29, 2023
0b0b23e
Add FederatedSAMLProfile to samlResponse method
devkiran Nov 30, 2023
726b7e7
Merge branch 'main' into feat/audit-logs-saas-app
devkiran Nov 30, 2023
71c6662
Update package-lock.json
devkiran Nov 30, 2023
ddb51c1
Remove console.log
devkiran Nov 30, 2023
48e7314
Merge branch 'main' into feat/audit-logs-saas-app
devkiran Jan 3, 2024
bb119bb
Add audit log teams configuration
devkiran Jan 3, 2024
de01aed
Merge branch 'main' into feat/audit-logs-saas-app
devkiran Jan 4, 2024
93e1199
Fix the build
devkiran Jan 4, 2024
e0c7592
Merge branch 'main' into feat/audit-logs-saas-app
devkiran Jan 4, 2024
a79cf7c
Remove console.log
devkiran Jan 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,9 @@ DSYNC_WEBHOOK_BATCH_SIZE=
# Google workspace directory sync
DSYNC_GOOGLE_CLIENT_ID=
DSYNC_GOOGLE_CLIENT_SECRET=
DSYNC_GOOGLE_REDIRECT_URI=
DSYNC_GOOGLE_REDIRECT_URI=

# Retraced
devkiran marked this conversation as resolved.
Show resolved Hide resolved
RETRACED_HOST_URL=
RETRACED_API_KEY=
RETRACED_PROJECT_ID=
11 changes: 9 additions & 2 deletions ee/branding/api/admin/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';

import jackson from '@lib/jackson';
import retraced from '@ee/retraced';

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req;
Expand All @@ -25,10 +26,16 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
const { brandingController } = await jackson();

const { logoUrl, faviconUrl, companyName, primaryColor } = req.body;
const brandingUpdated = await brandingController.update(req.body);

retraced.reportAdminPortalEvent({
action: 'portal.branding.update',
crud: 'u',
req,
});

return res.json({
data: await brandingController.update({ logoUrl, faviconUrl, companyName, primaryColor }),
data: brandingUpdated,
});
};

Expand Down
19 changes: 19 additions & 0 deletions ee/federated-saml/api/admin/[id]/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';

import jackson from '@lib/jackson';
import retraced from '@ee/retraced';

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req;
Expand Down Expand Up @@ -49,6 +50,15 @@ const handlePUT = async (req: NextApiRequest, res: NextApiResponse) => {

const updatedApp = await samlFederatedController.app.update(req.body);

retraced.reportAdminPortalEvent({
action: 'federation.app.update',
crud: 'u',
req,
target: {
id: updatedApp.id,
},
});

return res.status(200).json({ data: updatedApp });
};

Expand All @@ -60,6 +70,15 @@ const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {

await samlFederatedController.app.delete({ id });

retraced.reportAdminPortalEvent({
action: 'federation.app.delete',
crud: 'd',
req,
target: {
id,
},
});

return res.status(200).json({ data: {} });
};

Expand Down
10 changes: 10 additions & 0 deletions ee/federated-saml/api/admin/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';

import jackson from '@lib/jackson';
import retraced from '@ee/retraced';

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req;
Expand Down Expand Up @@ -28,6 +29,15 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {

const app = await samlFederatedController.app.create(req.body);

retraced.reportAdminPortalEvent({
action: 'federation.app.create',
crud: 'c',
req,
target: {
id: app.id,
},
});

return res.status(201).json({ data: app });
};

Expand Down
214 changes: 214 additions & 0 deletions ee/retraced/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import * as Retraced from '@retracedhq/retraced';
import type { Event } from '@retracedhq/retraced';
import type { NextApiRequest } from 'next';
import { getToken as getNextAuthToken } from 'next-auth/jwt';
import requestIp from 'request-ip';

import jackson from '@lib/jackson';
import { retracedOptions } from '@lib/env';
import { sessionName } from '@lib/constants';

type AuditEventType =
| 'sso.user.login'

// Single Sign On
| 'sso.connection.create'
| 'sso.connection.update'
| 'sso.connection.delete'

// Directory Sync
| 'dsync.connection.create'
| 'dsync.connection.update'
| 'dsync.connection.delete'
| 'dsync.webhook_event.delete'

// Setup Link
| 'sso.setuplink.create'
| 'sso.setuplink.update'
| 'sso.setuplink.delete'
| 'dsync.setuplink.create'
| 'dsync.setuplink.update'
| 'dsync.setuplink.delete'

// Federated SAML
| 'federation.app.create'
| 'federation.app.update'
| 'federation.app.delete'

// Retraced
| 'retraced.project.create'

// Admin settings
| 'portal.branding.update'
| 'portal.user.login';

interface ReportAdminEventParams {
action: AuditEventType;
crud: Retraced.CRUD;
target?: Retraced.Target;
req?: NextApiRequest;
actor?: Retraced.Actor;
}

interface ReportEventParams {
action: AuditEventType;
crud: Retraced.CRUD;
actor: Retraced.Actor;
req: NextApiRequest;
group?: Retraced.Group;
target?: Retraced.Target;
productId?: string;
}

const adminPortalGroup = {
id: 'boxyhq-admin-portal',
name: 'BoxyHQ Admin Portal',
};

let client: Retraced.Client | null = null;

// Create a Retraced client
const getClient = async () => {
const { checkLicense } = await jackson();

if (!(await checkLicense())) {
return;
}

if (!retracedOptions.hostUrl || !retracedOptions.apiKey || !retracedOptions.projectId) {
return;
}

if (client) {
return client;
}

client = new Retraced.Client({
endpoint: retracedOptions.hostUrl,
apiKey: retracedOptions.apiKey,
projectId: retracedOptions.projectId,
});

return client;
};

// Report events to Retraced
const reportEvent = async (params: ReportEventParams) => {
const { action, crud, actor, req } = params;

try {
const retracedClient = await getClient();

if (!retracedClient) {
return;
}

const retracedEvent: Event = {
action,
crud,
actor,
created: new Date(),
source_ip: getClientIp(req),
};

if ('group' in params && params.group) {
retracedEvent.group = params.group;
}

if ('target' in params && params.target) {
retracedEvent.target = params.target;
}

// Find team info if productId is provided
if ('productId' in params && params.productId) {
const { productController } = await jackson();

const product = await productController.get(params.productId);

retracedEvent.group = {
id: product.teamId,
name: product.teamName,
};

retracedEvent.target = {
id: product.id,
name: product.name,
};
}

await retracedClient.reportEvent(retracedEvent);
} catch (error: any) {
console.error('Error reporting event to Retraced', error);
}
};

// Report Admin portal events to Retraced
export const reportAdminPortalEvent = async (params: ReportAdminEventParams) => {
const { action, crud, target, actor, req } = params;

try {
const retracedClient = await getClient();

if (!retracedClient) {
return;
}

const retracedEvent: Event = {
action,
crud,
target,
actor: actor ?? (await getAdminUser(req)),
group: adminPortalGroup,
created: new Date(),
source_ip: getClientIp(req),
};

await retracedClient.reportEvent(retracedEvent);
} catch (error: any) {
console.error('Error reporting event to Retraced', error);
}
};

// Find admin actor info from NextAuth token
const getAdminUser = async (req: NextApiRequest | undefined) => {
if (!req) {
throw new Error(`NextApiRequest is required to get actor info for Retraced event.`);
}

const user = await getNextAuthToken({
req,
cookieName: sessionName,
});

if (!user || !user.email || !user.name) {
throw new Error(`Can't find actor info from the NextAuth token.`);
}

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

// Find Ip from request
const getClientIp = (req: NextApiRequest | undefined) => {
if (!req) {
return undefined;
}

const sourceIp = requestIp.getClientIp(req);

// TODO: Verify this is the correct way to check
if (!sourceIp.startsWith('::')) {
return sourceIp as string;
}

return undefined;
};

const retraced = {
reportEvent,
reportAdminPortalEvent,
};

export default retraced;
2 changes: 2 additions & 0 deletions lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const retraced = {
hostUrl: process.env.RETRACED_HOST_URL,
externalUrl: process.env.RETRACED_EXTERNAL_URL || process.env.RETRACED_HOST_URL,
adminToken: process.env.RETRACED_ADMIN_ROOT_TOKEN,
apiKey: process.env.RETRACED_API_KEY,
projectId: process.env.RETRACED_PROJECT_ID,
};

// Terminus
Expand Down
Loading
Loading