Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
This commit introduces a new module, `auditOwner.js`, which includes functions for resolving audit owner details and generating display names for actors. The `resolveAuditOwner` function determines the owner type based on the actor's object type, defaulting to 'user' if not specified. The `actorDisplayName` function formats the display name based on the actor's properties, enhancing the clarity of audit logs. Additionally, the `AUDIT_OWNER_TYPES` constant is exported for use in other modules. Updates to existing files incorporate these new functions for improved audit logging and notification handling.
216 lines
6.9 KiB
JavaScript
216 lines
6.9 KiB
JavaScript
/**
|
|
* Authentication middleware - uses Redis session store.
|
|
* Keycloak is used only for login/refresh; session validation is done via Redis.
|
|
*/
|
|
import config, { getEnvironment } from './config.js';
|
|
import log4js from 'log4js';
|
|
import NodeCache from 'node-cache';
|
|
import bcrypt from 'bcrypt';
|
|
import { userModel } from './database/schemas/management/user.schema.js';
|
|
import { appPasswordModel } from './database/schemas/management/apppassword.schema.js';
|
|
import { getObject } from './database/database.js';
|
|
import { hostModel } from './database/schemas/management/host.schema.js';
|
|
import { getSession, lookupUserByToken } from './services/misc/auth.js';
|
|
|
|
const logger = log4js.getLogger('Keycloak');
|
|
logger.level = config.server.logLevel || 'info';
|
|
|
|
const userCache = new NodeCache({ stdTTL: 300 });
|
|
|
|
userCache.on('expired', (key, value) => {
|
|
logger.debug(`Cache entry expired: ${key}`);
|
|
});
|
|
|
|
userCache.on('flush', () => {
|
|
logger.info('Cache flushed');
|
|
});
|
|
|
|
const lookupUser = async (preferredUsername) => {
|
|
try {
|
|
const cachedUser = userCache.get(preferredUsername);
|
|
if (cachedUser) {
|
|
logger.debug(`User found in cache: ${preferredUsername}`);
|
|
return cachedUser;
|
|
}
|
|
|
|
logger.debug(`User not in cache, querying database: ${preferredUsername}`);
|
|
const user = await userModel.findOne({ username: preferredUsername });
|
|
|
|
if (user) {
|
|
userCache.set(preferredUsername, user);
|
|
logger.debug(`User stored in cache: ${preferredUsername}`);
|
|
return user;
|
|
}
|
|
|
|
logger.warn(`User not found in database: ${preferredUsername}`);
|
|
return null;
|
|
} catch (error) {
|
|
logger.error(`Error looking up user ${preferredUsername}:`, error.message);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Middleware to check if the user is authenticated.
|
|
* Supports: 1) Bearer token (Redis session), 2) Bearer token (email-render JWT for Puppeteer),
|
|
* 3) x-host-id + x-auth-code (host auth)
|
|
*/
|
|
const isAuthenticated = async (req, res, next) => {
|
|
const authHeader = req.headers.authorization || req.headers.Authorization;
|
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
const token = authHeader.substring(7);
|
|
|
|
try {
|
|
const session = await getSession(token);
|
|
if (session && session.expiresAt > Date.now()) {
|
|
req.user = { ...session.user, _objectType: 'user' };
|
|
req.session = session;
|
|
return next();
|
|
}
|
|
|
|
// Try email-render JWT (short-lived token for Puppeteer email notifications)
|
|
const user = await lookupUserByToken(token);
|
|
if (user) {
|
|
req.user = { ...user, _objectType: 'user' };
|
|
req.session = { user };
|
|
return next();
|
|
}
|
|
} catch (error) {
|
|
logger.error('Session lookup error:', error.message);
|
|
}
|
|
}
|
|
|
|
// CURL/sandbox helper: when MARKETPLACE_DEBUG_TOKEN is set, Bearer that token
|
|
// impersonates the oldest Mongo user. Disabled unless the env var is present.
|
|
if (process.env.MARKETPLACE_DEBUG_TOKEN && authHeader && authHeader.startsWith('Bearer ')) {
|
|
const token = authHeader.substring(7);
|
|
if (token === process.env.MARKETPLACE_DEBUG_TOKEN) {
|
|
const user = await userModel.findOne({}).sort({ createdAt: 1 }).lean();
|
|
if (user) {
|
|
req.user = { ...user, _objectType: 'user' };
|
|
req.session = { user, debug: true };
|
|
return next();
|
|
}
|
|
}
|
|
}
|
|
|
|
const hostId = req.headers['x-host-id'];
|
|
const authCode = req.headers['x-auth-code'];
|
|
if (hostId && authCode) {
|
|
const host = await getObject({ model: hostModel, id: hostId });
|
|
if (host && host.authCode === authCode) {
|
|
req.user = { ...host, _objectType: 'host' };
|
|
return next();
|
|
}
|
|
}
|
|
logger.debug('Not authenticated', { hostId, authCode }, 'req.headers', req.headers);
|
|
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
|
};
|
|
|
|
const authenticateWithAppPassword = async (username, secret) => {
|
|
if (!username || !secret) return null;
|
|
|
|
const user = await userModel.findOne({ username }).lean();
|
|
if (!user) return null;
|
|
|
|
const appPasswords = await appPasswordModel
|
|
.find({ user: user._id, active: true })
|
|
.select('+secret')
|
|
.lean();
|
|
|
|
for (const appPassword of appPasswords) {
|
|
const storedHash = appPassword.secret;
|
|
if (storedHash && (await bcrypt.compare(secret, storedHash))) {
|
|
return user;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const isAppAuthenticated = async (req, res, next) => {
|
|
const authHeader = req.headers.authorization || req.headers.Authorization;
|
|
const apiKey = req.headers['x-api-key'];
|
|
const userParam = req.params.username;
|
|
const passwordParam = req.params.password;
|
|
|
|
logger.debug('App authentication request', {
|
|
hasBasicAuth: authHeader?.startsWith('Basic ') === true,
|
|
hasApiKey: Boolean(apiKey),
|
|
hasPasswordParam: Boolean(passwordParam),
|
|
userParam,
|
|
});
|
|
|
|
// Supports HTTP Basic Auth (username + app password secret)
|
|
if (authHeader?.startsWith('Basic ')) {
|
|
try {
|
|
logger.debug('Basic auth header present');
|
|
const base64Credentials = authHeader.substring(6);
|
|
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
|
|
const colonIndex = credentials.indexOf(':');
|
|
const username = credentials.substring(0, colonIndex).trim();
|
|
const secret = credentials.substring(colonIndex + 1).trim();
|
|
const user = await authenticateWithAppPassword(username, secret);
|
|
if (user) {
|
|
req.user = user;
|
|
req.session = { user };
|
|
return next();
|
|
}
|
|
} catch (error) {
|
|
logger.error('Basic auth error:', error.message);
|
|
}
|
|
}
|
|
const appPassword = apiKey || passwordParam;
|
|
if (appPassword && userParam) {
|
|
logger.debug('App password and username present', { userParam });
|
|
const user = await authenticateWithAppPassword(userParam, appPassword);
|
|
if (user) {
|
|
req.user = user;
|
|
req.session = { user };
|
|
return next();
|
|
}
|
|
}
|
|
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
|
};
|
|
|
|
const isAppQueryAuthenticated = async (req, res, next) => {
|
|
try {
|
|
const username = typeof req.query.u === 'string' ? req.query.u.trim() : '';
|
|
const secret = typeof req.query.p === 'string' ? req.query.p : '';
|
|
const user = await authenticateWithAppPassword(username, secret);
|
|
if (user) {
|
|
req.user = user;
|
|
req.session = { user };
|
|
return next();
|
|
}
|
|
} catch (error) {
|
|
logger.error('Query auth error:', error.message);
|
|
}
|
|
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
|
};
|
|
|
|
const clearUserCache = () => {
|
|
userCache.flushAll();
|
|
logger.info('User cache cleared');
|
|
};
|
|
|
|
const getUserCacheStats = () => {
|
|
return userCache.getStats();
|
|
};
|
|
|
|
const removeUserFromCache = (username) => {
|
|
userCache.del(username);
|
|
logger.debug(`User removed from cache: ${username}`);
|
|
};
|
|
|
|
export {
|
|
isAuthenticated,
|
|
isAppAuthenticated,
|
|
isAppQueryAuthenticated,
|
|
lookupUser,
|
|
clearUserCache,
|
|
getUserCacheStats,
|
|
removeUserFromCache,
|
|
getEnvironment,
|
|
};
|