/** * 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; };