Skip to content

Commit

Permalink
Promote from BLAIS5-4254 to main (#386)
Browse files Browse the repository at this point in the history
Update 05/11/24:

Took inspiration from the DQS logging functionality

Added server logging using GCP logging library:
Creating a new user
Changing a user's password
Changing a user's role
Deleting a user

Refactored code and improved unit tests:
Changed nodejs backend folder structure for better readability
Included log assertions in test cases
Added new unit tests to cover different scenarios

Out of scope improvements:
Use latest improved blaise-login library
Ensure text inputs are sanitized and trimmed (remove trailling whitespaces) before submitting data
Added unit tests to cover 'bad' text inputs or text inputs with whitespaces 

---------

Co-authored-by: kristian4res <[email protected]>
  • Loading branch information
1 parent 8e94f82 commit 8e1a250
Show file tree
Hide file tree
Showing 19 changed files with 833 additions and 453 deletions.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,23 @@
"formik": "2.4.2"
},
"dependencies": {
"@google-cloud/logging": "^9.6.0",
"@google-cloud/profiler": "^4.1.1",
"@testing-library/dom": "^8.3.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.7.4",
"blaise-api-node-client": "git+https://github.com/ONSdigital/blaise-api-node-client",
"blaise-design-system-react-components": "git+https://github.com/ONSdigital/blaise-design-system-react-components#0.14.0",
"blaise-login-react": "git+https://github.com/ONSdigital/blaise-login-react#1.1.0",
"blaise-design-system-react-components": "git+https://github.com/ONSdigital/blaise-design-system-react-components#0.14.1",
"blaise-login-react": "git+https://github.com/ONSdigital/blaise-login-react#1.1.1",
"dotenv": "^10.0.0",
"ejs": "^3.1.10",
"express": "^4.19.2",
"formik": "2.4.2",
"history": "^4.9.0",
"jest-cucumber": "^3.0.0",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"multer": "^1.4.2",
"number-to-words": "^1.2.4",
Expand Down
125 changes: 0 additions & 125 deletions server/BlaiseAPI/index.ts

This file was deleted.

8 changes: 4 additions & 4 deletions server/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import GetNodeServer from "./server";
import pino from "pino";
import { loadConfigFromEnv } from "./Config";
import BlaiseApiClient from "blaise-api-node-client";
import { Auth } from "blaise-login-react/blaise-login-react-server";
import dotenv from "dotenv";
import createLogger from "./logger/pinoLogger";

const port: string = process.env.PORT || "5002";
const logger = pino();

if (process.env.NODE_ENV !== "production") {
dotenv.config();
}
const config = loadConfigFromEnv();
const blaiseApiClient = new BlaiseApiClient(config.BlaiseApiUrl);
const auth = new Auth(config);
const server = GetNodeServer(config, blaiseApiClient, auth);
const pinoLogger = createLogger();
const server = GetNodeServer(config, blaiseApiClient, auth, pinoLogger);
server.listen(port);

logger.info("App is listening on port " + port);
pinoLogger.logger.info("BAM nodejs server is listening on port " + port);
6 changes: 6 additions & 0 deletions server/interfaces/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface AuditLog {
id: string;
timestamp: string;
message: string;
severity: string;
}
1 change: 0 additions & 1 deletion server/interfaces/server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AuthConfig } from "blaise-login-react/blaise-login-react-server";
import { string } from "prop-types";

export interface CustomConfig extends AuthConfig {
BlaiseApiUrl: string
Expand Down
59 changes: 59 additions & 0 deletions server/logger/cloudLogging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { Logging } from "@google-cloud/logging";
import { IncomingMessage } from "http";
import { AuditLog } from "../interfaces/logger";

export function formatLogMessage(text: string): string {
const message = text.replace(/[^\x20-\x7E\r\n]+/g, "");
const logFormat = "AUDIT_LOG: message";
return logFormat.replace("message", message);
}

export default class AuditLogger {
projectId: string;
logger: Logging;
logName: string;

constructor(projectId: string) {
this.projectId = projectId;
this.logger = new Logging({ projectId: this.projectId });
this.logName = `projects/${this.projectId}/logs/stdout`;
}

info(logger: IncomingMessage["log"], message: string): void {
const log = formatLogMessage(message);
logger.info(log);
}

error(logger: IncomingMessage["log"], message: string): void {
const log = formatLogMessage(message);
logger.error(log);
}

async getLogs(): Promise<AuditLog[]> {
const auditLogs: AuditLog[] = [];
const log = this.logger.log(this.logName);
const [entries] = await log.getEntries({ filter: "jsonPayload.message=~\"^AUDIT_LOG: \"", maxResults: 50 });
for (const entry of entries) {
let id = "";
let timestamp = "";
let severity = "INFO";
if (entry.metadata.insertId != null) {
id = entry.metadata.insertId;
}
if (entry.metadata.timestamp != null) {
timestamp = entry.metadata.timestamp.toString();
}
if (entry.metadata.severity != null) {
severity = entry.metadata.severity.toString();
}
auditLogs.push({
id: id,
timestamp: timestamp,
message: entry.data.message.replace(/^AUDIT_LOG: /, ""),
severity: severity
});
}
return auditLogs;
}
}
File renamed without changes.
148 changes: 148 additions & 0 deletions server/routes/blaiseApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import express, { Request, Response, Router } from "express";
import { CustomConfig } from "../interfaces/server";
import { Auth } from "blaise-login-react/blaise-login-react-server";
import BlaiseApiClient from "blaise-api-node-client";
import AuditLogger from "../logger/cloudLogging";

export default function blaiseApi(config: CustomConfig, auth: Auth, blaiseApiClient: BlaiseApiClient, auditLogger: AuditLogger): Router {
const router = express.Router();

router.get("/api/roles", auth.Middleware, async function (req: Request, res: Response) {
res.status(200).json(await blaiseApiClient.getUserRoles());
});

router.get("/api/users", auth.Middleware, async function (req: Request, res: Response) {
res.status(200).json(await blaiseApiClient.getUsers());
});

router.patch("/api/users/:user/rolesAndPermissions", auth.Middleware, async function (req: Request, res: Response) {
const currentUser = auth.GetUser(auth.GetToken(req));
const { role } = req.body;
const user = req.params.user;
let newServerParks: string[];
let newDefaultServerPark: string;

if (!user || !role) {
return res.status(400).json("No user or role provided");
}

const roleServerParksOverride = config.RoleToServerParksMap[role];
if (roleServerParksOverride != null) {
newServerParks = roleServerParksOverride;
newDefaultServerPark = roleServerParksOverride[0];
} else {
const defaultServerPark = config.RoleToServerParksMap["DEFAULT"];
newServerParks = defaultServerPark;
newDefaultServerPark = defaultServerPark[0];
}

try {
await blaiseApiClient.changeUserRole(user, role);
await blaiseApiClient.changeUserServerParks(user, newServerParks, newDefaultServerPark);
const successMessage = `${currentUser.name || "Unknown user"} has successfully updated user role and permissions to ${role} for ${user}`;
auditLogger.info(req.log, successMessage);
return res.status(200).json({
message: "Successfully updated user role and permissions to " + role + " for " + user
});
} catch (error) {
const errorMessage = `Error whilst trying to update user role and permissions to ${role} for ${req.params.user}, with error message: ${error}`;
auditLogger.info(req.log, `${currentUser.name || "Unknown user"} has failed to update user role and permissions to ${role} for ${user}`);
auditLogger.error(req.log, errorMessage);
return res.status(500).json({
message: "Failed to update user role and permissions to " + role + " for " + user
});
}
});

router.get("/api/users/:user", auth.Middleware, async function (req: Request, res: Response) {
if (!req.params.user) {
return res.status(400).json("No user provided");
}

try {
const user = await blaiseApiClient.getUser(req.params.user);
const successMessage = `Successfully fetched user details for ${req.params.user}`;
return res.status(200).json({
message: successMessage,
data: user
});
} catch (error) {
const errorMessage = `Error whilst trying to retrieve user ${req.params.user}: ${error}`;
return res.status(500).json({
message: errorMessage,
error: error
});
}
});

router.get("/api/change-password/:user", auth.Middleware, async function (req: Request, res: Response) {
const currentUser = auth.GetUser(auth.GetToken(req));
let { password } = req.headers;

if (Array.isArray(password)) {
password = password.join("");
}

if (!req.params.user || !password) {
return res.status(400).json("No user or password provided");
}

blaiseApiClient.changePassword(req.params.user, password).then(() => {
auditLogger.info(req.log, `${currentUser.name || "Unknown"} has successfully changed the password for ${req.params.user}`);
return res.status(204).json(null);
}).catch((error: unknown) => {
auditLogger.info(req.log, `${currentUser.name || "Unknown"} has failed to change the password for ${req.params.user}`);
auditLogger.error(req.log, `Error whilst trying to change password for ${req.params.user}: ${error}`);
return res.status(500).json(error);
});
});

router.delete("/api/users", auth.Middleware, async function (req: Request, res: Response) {
try {
const currentUser = auth.GetUser(auth.GetToken(req));
let { user } = req.headers;

if (Array.isArray(user)) {
user = user.join("");
}

if (!user) {
auditLogger.error(req.log, "No user provided for deletion");
return res.status(400).json();
}
auditLogger.info(req.log, `${currentUser.name || "Unknown"} has successfully deleted user called ${user}`);
return res.status(204).json(await blaiseApiClient.deleteUser(user));
} catch (error) {
auditLogger.error(req.log, `Error whilst trying to delete user, ${req.headers.user}, with error message: ${error}`);
return res.status(500).json(error);
}
});

router.post("/api/users", auth.Middleware, async function (req: Request, res: Response) {
try {
const currentUser = auth.GetUser(auth.GetToken(req));
const data = req.body;

if(!data.role) {
return res.status(400).json({ message: "No role provided for user creation" });
}

const roleServerParksOverride = config.RoleToServerParksMap[data.role];
if (roleServerParksOverride != null) {
data.serverParks = roleServerParksOverride;
data.defaultServerPark = roleServerParksOverride[0];
} else {
const defaultServerPark = config.RoleToServerParksMap["DEFAULT"];
data.serverParks = defaultServerPark;
data.defaultServerPark = defaultServerPark[0];
}
auditLogger.info(req.log, `${currentUser.name || "Unknown"} has successfully created user, ${data.name}, with an assigned role of ${data.role}`);
return res.status(200).json(await blaiseApiClient.createUser(data));
} catch (error) {
auditLogger.error(req.log, `Error whilst trying to create new user, ${req.body.name}, with error message: ${error}`);
return res.status(500).json(error);
}
});

return router;
}
Loading

0 comments on commit 8e1a250

Please sign in to comment.