Initial commit
This commit is contained in:
267
lib/composable-security.js
Normal file
267
lib/composable-security.js
Normal file
@ -0,0 +1,267 @@
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
Reference in New Issue
Block a user