268 lines
9.2 KiB
JavaScript
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;
|
|
};
|