Skip to content

Commit

Permalink
feat: add site audit logs generator
Browse files Browse the repository at this point in the history
  • Loading branch information
dcshzj committed Mar 4, 2024
1 parent 01776f1 commit 8af8e27
Show file tree
Hide file tree
Showing 11 changed files with 692 additions and 1 deletion.
4 changes: 4 additions & 0 deletions .aws/deploy/backend-task-definition.prod.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@
{
"name": "SITE_CHECKER_FORM_KEY",
"valueFrom": "PROD_SITE_CHECKER_FORM_KEY"
},
{
"name": "SITE_AUDIT_LOGS_FORM_KEY",
"valueFrom": "PROD_SITE_AUDIT_LOGS_FORM_KEY"
}
],
"logConfiguration": {
Expand Down
4 changes: 4 additions & 0 deletions .aws/deploy/backend-task-definition.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@
{
"name": "SITE_CHECKER_FORM_KEY",
"valueFrom": "STAGING_SITE_CHECKER_FORM_KEY"
},
{
"name": "SITE_AUDIT_LOGS_FORM_KEY",
"valueFrom": "STAGING_SITE_AUDIT_LOGS_FORM_KEY"
}
],
"logConfiguration": {
Expand Down
1 change: 1 addition & 0 deletions .platform/hooks/predeploy/06_fetch_ssm_parameters.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ ENV_VARS=(
"UPTIME_ROBOT_API_KEY"
"GGS_REPAIR_FORM_KEY"
"SITE_CHECKER_FORM_KEY"
"SITE_AUDIT_LOGS_FORM_KEY"
)

echo "Set AWS region"
Expand Down
7 changes: 7 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,13 @@ const config = convict({
format: "required-string",
default: "",
},
siteAuditLogsFormKey: {
doc: "FormSG API key for site audit logs form",
env: "SITE_AUDIT_LOGS_FORM_KEY",
sensitive: true,
format: "required-string",
default: "",
},
},
postman: {
apiKey: {
Expand Down
4 changes: 4 additions & 0 deletions src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export const EFS_VOL_PATH_STAGING_LITE = path.join(
config.get("aws.efs.volPath"),
"repos-lite"
)
export const EFS_VOL_PATH_AUDIT_LOGS = path.join(
config.get("aws.efs.volPath"),
"audit-logs"
)
export const STAGING_BRANCH = "staging"
export const STAGING_LITE_BRANCH = "staging-lite"
export const PLACEHOLDER_FILE_NAME = ".keep"
Expand Down
11 changes: 11 additions & 0 deletions src/errors/AuditLogsError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BaseIsomerError } from "./BaseError"

export default class AuditLogsError extends BaseIsomerError {
constructor(message: string) {
super({
status: 500,
code: "AuditLogsError",
message,
})
}
}
128 changes: 128 additions & 0 deletions src/routes/formsg/formsgSiteAuditLogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/* eslint-disable import/prefer-default-export */
import type { DecryptedContentAndAttachments } from "@opengovsg/formsg-sdk/dist/types"
import express, { RequestHandler } from "express"

import { config } from "@root/config/config"
import InitializationError from "@root/errors/InitializationError"
import logger from "@root/logger/logger"
import { attachFormSGHandler } from "@root/middleware"
import AuditLogsService from "@root/services/admin/AuditLogsService"
import { getField, getFieldsFromTable } from "@root/utils/formsg-utils"

interface FormsgSiteAuditLogsRouterProps {
auditLogsService: AuditLogsService
}

const SITE_AUDIT_LOGS_FORM_KEY = config.get("formSg.siteAuditLogsFormKey")

const REQUESTER_EMAIL_FIELD = "Where should we send the email address to?"
const REPO_NAME_FIELD =
"What is the name of the Isomer site that you need logs for? (Repo Name (in GitHub))"
const LOGS_TIMEFRAME_FIELD = "I need a log of edits made in:"
const LOGS_TIMEFRAME_START_FIELD = "Start date"
const LOGS_TIMEFRAME_END_FIELD = "End date"

export class FormsgSiteAuditLogsRouter {
private readonly auditLogsService: FormsgSiteAuditLogsRouterProps["auditLogsService"]

constructor({ auditLogsService }: FormsgSiteAuditLogsRouterProps) {
this.auditLogsService = auditLogsService
}

getAuditLogsHandler: RequestHandler<
never,
Record<string, never>,
{ data: { submissionId: string } },
never,
{ submission: DecryptedContentAndAttachments }
> = async (req, res) => {
let startDate = "1970-01-01"
let endDate = new Date().toISOString().split("T")[0]
const repoNames: Set<string> = new Set()

const { responses } = res.locals.submission.content

const requesterEmail = getField(responses, REQUESTER_EMAIL_FIELD)

if (!requesterEmail) {
logger.error(
"No requester email was provided in site audit logs form submission"
)
return
}

const repoNamesFromTable = getFieldsFromTable(responses, REPO_NAME_FIELD)

if (!repoNamesFromTable) {
logger.error(
"No repo names were provided in site audit logs form submission"
)
return
}

repoNamesFromTable.forEach((repoName) => {
if (typeof repoName === "string") {
// actually wont happen based on our formsg form, but this code
// is added defensively
repoNames.add(repoName)
} else {
repoNames.add(repoName[0])
}
})

const logsTimeframe = getField(responses, LOGS_TIMEFRAME_FIELD)

if (logsTimeframe === "The past calendar year") {
startDate = `${new Date().getFullYear() - 1}-01-01`
endDate = `${new Date().getFullYear() - 1}-12-31`
} else if (logsTimeframe === "The past calendar month") {
const startDateObject = new Date()
startDateObject.setMonth(startDateObject.getMonth() - 1)
const endDateObject = new Date()
endDateObject.setDate(0)

startDate = `${startDateObject.getFullYear()}-${startDateObject
.getMonth()
.toString()
.padStart(2, "0")}-01`
endDate = `${endDateObject.getFullYear()}-${endDateObject
.getMonth()
.toString()
.padStart(2, "0")}-${endDateObject.getDate()}`
} else {
const startDateField = getField(responses, LOGS_TIMEFRAME_START_FIELD)
const endDateField = getField(responses, LOGS_TIMEFRAME_END_FIELD)
if (startDateField && endDateField) {
startDate = startDateField
endDate = endDateField
}
}

res.sendStatus(200)

this.auditLogsService.getAuditLogsViaFormsg(
requesterEmail,
Array.from(repoNames),
startDate,
endDate,
req.body.data.submissionId
)
}

getRouter() {
const router = express.Router({ mergeParams: true })
if (!SITE_AUDIT_LOGS_FORM_KEY) {
throw new InitializationError(
"Required SITE_AUDIT_LOGS_FORM_KEY environment variable is not defined"
)
}

router.post(
"/audit-logs",
attachFormSGHandler(SITE_AUDIT_LOGS_FORM_KEY),
this.getAuditLogsHandler
)

return router
}
}
17 changes: 16 additions & 1 deletion src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,12 @@ import { mailer } from "@services/utilServices/MailClient"
import { database } from "./database/config"
import { apiLogger } from "./middleware/apiLogger"
import { NotificationOnEditHandler } from "./middleware/notificationOnEditHandler"
import { FormsgSiteAuditLogsRouter } from "./routes/formsg/formsgSiteAuditLogs"
import getAuthenticatedSubrouter from "./routes/v2/authenticated"
import { ReviewsRouter } from "./routes/v2/authenticated/review"
import getAuthenticatedSitesSubrouter from "./routes/v2/authenticatedSites"
import { SgidAuthRouter } from "./routes/v2/sgidAuth"
import AuditLogsService from "./services/admin/AuditLogsService"
import RepoManagementService from "./services/admin/RepoManagementService"
import GitFileCommitService from "./services/db/GitFileCommitService"
import GitFileSystemService from "./services/db/GitFileSystemService"
Expand All @@ -88,8 +90,8 @@ import CollaboratorsService from "./services/identity/CollaboratorsService"
import LaunchClient from "./services/identity/LaunchClient"
import LaunchesService from "./services/identity/LaunchesService"
import DynamoDBDocClient from "./services/infra/DynamoDBClient"
import ReviewCommentService from "./services/review/ReviewCommentService"
import RepoCheckerService from "./services/review/RepoCheckerService"
import ReviewCommentService from "./services/review/ReviewCommentService"
import { rateLimiter } from "./services/utilServices/RateLimiter"
import SgidAuthService from "./services/utilServices/SgidAuthService"
import { isSecure } from "./utils/auth-utils"
Expand Down Expand Up @@ -315,6 +317,14 @@ const repoCheckerService = new RepoCheckerService({
git: simpleGitInstance,
})

const auditLogsService = new AuditLogsService({
collaboratorsService,
isomerAdminsService,
reviewRequestService,
sitesService,
usersService,
})

// poller site launch updates
infraService.pollMessages()

Expand Down Expand Up @@ -399,6 +409,10 @@ const formsgSiteCheckerRouter = new FormsgSiteCheckerRouter({
repoCheckerService,
})

const formsgSiteAuditLogsRouter = new FormsgSiteAuditLogsRouter({
auditLogsService,
})

const app = express()

if (isSecure) {
Expand Down Expand Up @@ -440,6 +454,7 @@ app.use("/formsg", formsgSiteCreateRouter.getRouter())
app.use("/formsg", formsgSiteLaunchRouter.getRouter())
app.use("/formsg", formsgGGsRepairRouter.getRouter())
app.use("/formsg", formsgSiteCheckerRouter.getRouter())
app.use("/formsg", formsgSiteAuditLogsRouter.getRouter())

// catch unknown routes
app.use((req, res, next) => {
Expand Down
Loading

0 comments on commit 8af8e27

Please sign in to comment.