Files
wiki-security-composable/lib/composable-security.js
2025-08-02 02:48:51 -05:00

268 lines
9.2 KiB
JavaScript

/**
* Composable Security Plugin for Federated Wiki
*
* Combines authentication providers with authorization enhancers
* while maintaining compatibility with the existing security interface.
*/
// Register CoffeeScript to allow requiring .coffee files. Needed for wiki-security-passportjs
require('coffeescript/register');
const _ = require('lodash');
const coffee = require('coffeescript');
module.exports = (log, loga, argv) => {
const security = {};
// Parse security configuration
const authProvider = argv.auth_provider;
let authzEnhancers = argv.authz_enhancers || [];
// Ensure authzEnhancers is always an array
if (typeof authzEnhancers === 'string') {
authzEnhancers = [authzEnhancers];
}
// Require explicit auth provider configuration
if (!authProvider) {
const errorMsg = 'Composable Security: auth_provider must be specified. Example: wiki --security_type composable --auth_provider wiki-security-friends';
log(errorMsg);
throw new Error(errorMsg);
}
log(`Composable Security: Loading auth provider: ${authProvider}`);
if (authzEnhancers.length > 0) {
log(`Composable Security: Loading authz enhancers: ${authzEnhancers.join(', ')}`);
}
// Load the base authentication provider
let baseHandler;
try {
// Check if auth provider is already a function (for testing)
if (typeof authProvider === 'function') {
baseHandler = authProvider(log, loga, argv);
} else {
const AuthProvider = require(authProvider);
baseHandler = AuthProvider(log, loga, argv);
}
} catch (error) {
log(`Failed to load auth provider ${authProvider}: ${error.message}`);
throw new Error(`Cannot load authentication provider: ${authProvider}`);
}
// Load authorization enhancers from regular plugins
const enhancers = [];
for (const enhancerName of authzEnhancers) {
try {
// Check if enhancer is already a function (for testing)
let enhancer;
if (typeof enhancerName === 'function') {
enhancer = enhancerName(log, loga, argv, baseHandler);
} else {
// Load as a regular plugin
let Plugin;
try {
// First try direct require (for global modules)
Plugin = require(enhancerName);
} catch (firstError) {
// Try resolving from the main wiki's package directory
const path = require('path');
const enhancerPath = path.join(argv.packageDir, enhancerName);
Plugin = require(enhancerPath);
}
// Check if the plugin exports a security enhancer
if (Plugin.securityEnhancer && typeof Plugin.securityEnhancer === 'function') {
enhancer = Plugin.securityEnhancer(log, loga, argv, baseHandler);
log(`Loaded security enhancer from plugin: ${enhancerName}`);
} else {
log(`Warning: Plugin ${enhancerName} does not export a securityEnhancer function`);
continue;
}
}
enhancers.push({
name: typeof enhancerName === 'string' ? enhancerName : enhancerName.name || 'anonymous',
handler: enhancer
});
log(`Loaded authorization enhancer: ${typeof enhancerName === 'string' ? enhancerName : enhancerName.name || 'anonymous'}`);
} catch (error) {
log(`Warning: Failed to load authz enhancer ${enhancerName}: ${error.message}`);
// Continue loading other enhancers
}
}
// Compose the security interface
// Owner management - delegate to base auth provider
security.retrieveOwner = (cb) => {
return baseHandler.retrieveOwner(cb);
};
security.getOwner = () => {
return baseHandler.getOwner();
};
security.setOwner = (id, cb) => {
return baseHandler.setOwner(id, cb);
};
// User identification - allow enhancers to modify
security.getUser = (req) => {
let baseGetUser = () => baseHandler.getUser(req);
// Let each enhancer potentially modify user identification
for (const enhancer of enhancers) {
if (enhancer.handler.getUser) {
const previousGetUser = baseGetUser;
baseGetUser = () => enhancer.handler.getUser(req, previousGetUser);
}
}
return baseGetUser();
};
// Authorization - compose base auth with enhancers
security.isAuthorized = (req) => {
let baseIsAuthorized = () => baseHandler.isAuthorized(req);
// Let each enhancer potentially modify authorization
for (const enhancer of enhancers) {
if (enhancer.handler.isAuthorized) {
const previousIsAuthorized = baseIsAuthorized;
baseIsAuthorized = () => enhancer.handler.isAuthorized(req, previousIsAuthorized);
}
}
return baseIsAuthorized();
};
// Admin check - compose base auth with enhancers
security.isAdmin = (req) => {
let baseIsAdmin = () => baseHandler.isAdmin(req);
// Let each enhancer potentially modify admin check
for (const enhancer of enhancers) {
if (enhancer.handler.isAdmin) {
const previousIsAdmin = baseIsAdmin;
baseIsAdmin = () => enhancer.handler.isAdmin(req, previousIsAdmin);
}
}
return baseIsAdmin();
};
// Clean and setup client assets from base provider
const setupClientAssets = () => {
if (typeof authProvider === 'string') {
try {
const path = require('path');
const fs = require('fs');
// Get base provider's client path
const packagePath = require.resolve(`${authProvider}/package.json`);
const baseProviderClientPath = path.join(path.dirname(packagePath), 'client');
if (fs.existsSync(baseProviderClientPath)) {
// Get our client path
const ourClientPath = path.join(__dirname, '..', 'client');
// Clean existing client directory to avoid stale files from previous providers
if (fs.existsSync(ourClientPath)) {
log(`Composable Security: Cleaning existing client directory`);
fs.rmSync(ourClientPath, { recursive: true, force: true });
}
// Copy files from base provider to our client directory
const copyFiles = (source, target) => {
if (!fs.existsSync(target)) {
fs.mkdirSync(target, { recursive: true });
}
const files = fs.readdirSync(source);
for (const file of files) {
const sourcePath = path.join(source, file);
const targetPath = path.join(target, file);
if (fs.statSync(sourcePath).isDirectory()) {
copyFiles(sourcePath, targetPath);
} else {
if (path.extname(sourcePath) === '.coffee') {
const coffeeScript = fs.readFileSync(sourcePath, 'utf8');
const javascript = coffee.compile(coffeeScript);
const jsPath = targetPath.replace(/\.coffee$/, '.js');
fs.writeFileSync(jsPath, javascript);
log(`Compiled and copied ${file} as ${path.basename(jsPath)}`);
} else {
fs.copyFileSync(sourcePath, targetPath);
}
}
}
};
copyFiles(baseProviderClientPath, ourClientPath);
log(`Composable Security: Copied client assets from ${authProvider}`);
} else {
log(`Warning: No client directory found for ${authProvider}, client-side functionality may not work`);
}
} catch (error) {
log(`Warning: Failed to setup client assets: ${error.message}`);
}
}
};
// Set up client assets before defining routes
setupClientAssets();
// Route definition - combine base routes with enhancer routes
security.defineRoutes = (app, cors, updateOwner) => {
// Note: Static route for /security is automatically set up by wiki-server
// based on argv.security_type, so we don't need to set it up here.
// First, let the base auth provider define its routes
baseHandler.defineRoutes(app, cors, updateOwner);
// Apply middleware from enhancers
for (const enhancer of enhancers) {
if (enhancer.handler.middleware) {
log(`Adding middleware from ${enhancer.name}`);
app.use(enhancer.handler.middleware);
}
}
// Then, let each enhancer add additional routes
for (const enhancer of enhancers) {
if (enhancer.handler.defineRoutes) {
log(`Adding routes from ${enhancer.name}`);
enhancer.handler.defineRoutes(app, cors, updateOwner);
}
}
};
// Expose loaded components for debugging/introspection
security._debug = {
authProvider: authProvider,
authzEnhancers: authzEnhancers,
loadedEnhancers: enhancers.map(e => e.name),
baseHandler: baseHandler
};
// Expose the base provider's client path for the server to use
security.getClientPath = () => {
const path = require('path');
try {
if (typeof authProvider === 'string') {
// For npm packages, resolve the package directory
const packagePath = require.resolve(`${authProvider}/package.json`);
return path.join(path.dirname(packagePath), 'client');
}
} catch (error) {
log(`Warning: Could not resolve client path for ${authProvider}: ${error.message}`);
}
return null;
};
return security;
};