Initial commit

This commit is contained in:
2025-08-02 02:48:51 -05:00
commit 42255ba7f7
15 changed files with 3958 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
/client

506
README.md Normal file
View File

@ -0,0 +1,506 @@
# Wiki Security Composable
## Description
**`wiki-security-composable` is a composable security plugin that separates authentication from authorization** while maintaining full compatibility with the existing wiki server.
This architecture allows you to mix and match authentication providers (like friends or passportjs) with multiple authorization enhancers (like user access tokens, rate limiting, etc.) to create a flexible security system.
### Components
#### Security Plugin
**`wiki-security-composable`** - The foundation plugin that:
- Loads a base authentication provider (friends, passportjs, etc.)
- Loads multiple authorization enhancers
- Composes them into a single security interface
- Maintains compatibility with existing wiki server expectations
The plugin uses a **function composition pattern** where authorization enhancers wrap the base authentication provider's methods. Each enhancer receives both the original request and a reference to the previous function in the chain, allowing it to call, modify, or completely override the previous behavior. This creates a middleware-like system where enhancers can participate in user identification, authorization checks, and admin verification.
**Order matters**: Enhancers are processed sequentially, with each one wrapping the previous function. The final composed function represents a chain where the last enhancer is called first. This means enhancers later in the configuration list can override decisions of earlier enhancers. For example, a rate limiting enhancer should typically come last to ensure it can block requests regardless of other authorization decisions.
#### Authorization Enhancers
Authorization enhancers are implemented as regular `wiki-plugin-*` packages that export a `securityEnhancer` function. This approach integrates seamlessly with the existing plugin system and allows plugins to provide both UI components and server-side functionality.
**`wiki-plugin-useraccesstokens`** - Plugin that adds API token authentication and provides token management (create, list, revoke, delete) for any base authentication provider.
**`wiki-plugin-ratelimit`** - Plugin that adds request rate limiting to prevent abuse and ensure fair usage.
### Context
Security plugins in FedWiki have traditionally been monolithic, making it difficult to extend or customize security features. This composable architecture addresses that by allowing developers to create modular security components that can be combined as needed.
The need for this arose from the desire to add new security features without modifying the core wiki server or existing plugins. By separating authentication from authorization, we can create a flexible system that allows for easy extension and customization.
This composable architecture could later be adopted by the core FedWiki server, but now it serves as a proof of concept and practical implementation for developers to build upon.
#### Other approaches considered
In our quest to add User Access Tokens and Rate Limiting to FedWiki, we explored several approaches:
1. **Stacking Wrapper Security Plugins**: This approach involved creating new plugins that wrapped existing authentication plugins. As an example, we could create a `wiki-security-useraccesstokens` plugin that wraps the `wiki-security-passportjs` plugin to add token support. While this would allow for adding multiple authorization features to an existing plugin, it would work by stacking wrapper plugins, which could lead to complexity and compatibility issues. We didn't want a situation where you would have to wrap `wiki-security-friends` with `wiki-security-useraccesstokens` and then wrap that with `wiki-security-ratelimit`, etc.
2. **Multiple Security Plugins**: This approach involved creating authorization enhancers as security plugins that could be installed alongside existing authentication plugins. For example, we could have a `wiki-security-useraccesstokens` plugin that works with any base authentication provider security plugin. This would've been implemented by making changes to the core FedWiki server to allow multiple security plugins to be loaded and composed together, making no distinction between authentication and authorization. This would have required significant changes to the core server and made it very complex to manage multiple security plugins and their interactions.
We ultimately decided against these approaches because they either added unnecessary complexity. Instead, we opted for a composable security architecture that separates authentication from authorization while maintaining full compatibility with the wiki server and existing plugins.
## Usage
### Basic Setup (Friends + Tokens)
Install the necessary components:
```bash
npm install wiki-security-composable wiki-plugin-useraccesstokens
```
Configure wiki `config.json`:
```JSON
{
"security_type": "wiki-security-composable",
"auth_provider": "wiki-security-friends",
"authz_enhancers": ["wiki-plugin-useraccesstokens"]
}
```
Run the wiki server:
```bash
wiki --security_type composable
```
Or configure directly from command line:
```bash
wiki --security_type composable --auth_provider wiki-security-friends --authz_enhancers wiki-plugin-useraccesstokens
```
### Co-op Setup (OAuth + Tokens + Rate Limiting)
Install the necessary components:
```bash
npm install wiki-security-composable wiki-plugin-useraccesstokens wiki-plugin-ratelimit
```
Configure wiki `config.json`:
```JSON
{
"security_type": "wiki-security-composable",
"auth_provider": "wiki-security-passportjs",
"authz_enhancers": [
"wiki-plugin-useraccesstokens",
"wiki-plugin-ratelimit"
],
"ratelimit_config": {
"windowMs": 900000, // 15 minutes
"maxRequests": 1000,
"maxAuthRequests": 5
}
}
```
### Migration from Existing Setups
#### From Friends Plugin
Current setup:
```JSON
{
"security_type": "wiki-security-friends"
}
```
Migrated setup (no functional change):
```JSON
{
"security_type": "wiki-security-composable",
"auth_provider": "wiki-security-friends"
}
```
Enhanced setup (adds API tokens):
```JSON
{
"security_type": "wiki-security-composable",
"auth_provider": "wiki-security-friends",
"authz_enhancers": ["wiki-plugin-useraccesstokens"]
}
```
#### From PassportJS Plugin
Current setup:
```JSON
{
"security_type": "wiki-security-passportjs",
"google_clientid": "...",
"google_clientsecret": "..."
}
```
Migrated setup:
```JSON
{
"security_type": "wiki-security-composable",
"auth_provider": "wiki-security-passportjs",
"google_clientid": "...",
"google_clientsecret": "..."
}
```
## Development
### Creating New Authentication Providers
To create a new authentication provider, implement the standard security plugin interface:
```javascript
module.exports = (log, loga, argv) => ({
retrieveOwner(cb),
getOwner(),
setOwner(id, cb),
getUser(req),
isAuthorized(req),
isAdmin(req),
defineRoutes(app, cors, updateOwner)
})
```
### Creating New Authorization Enhancers
Authorization enhancers are implemented as regular plugins by exporting a `securityEnhancer` function.
Create a regular `wiki-plugin-*` package and export a `securityEnhancer` function:
```javascript
// index.js - Main plugin entry point
export const startServer = (params) => {
// Regular plugin server functionality (optional)
console.log('Plugin server started')
}
export const securityEnhancer = (log, loga, argv, baseHandler) => {
const enhancer = {}
enhancer.getUser = (req, baseGetUser) => {
// Call base handler to get user
return baseGetUser()
}
// Override authorization to check additional permissions
enhancer.isAuthorized = (req, baseIsAuthorized) => {
const baseResult = baseIsAuthorized()
if (!baseResult) return false
// Additional authorization logic here
return checkCustomPermissions(req)
}
// Override admin check
enhancer.isAdmin = (req, baseIsAdmin) => {
const baseResult = baseIsAdmin()
if (!baseResult) return false
// Additional admin checks
return checkAdminPermissions(req)
}
// Add middleware (optional)
enhancer.middleware = (req, res, next) => {
// Custom middleware logic
next()
}
// Add additional routes (optional)
enhancer.defineRoutes = (app, cors, updateOwner) => {
app.get('/auth/custom', (req, res) => {
res.json({ message: 'Custom endpoint' })
})
}
return enhancer
}
```
Then use it in configuration:
```json
{
"security_type": "wiki-security-composable",
"auth_provider": "wiki-security-friends",
"authz_enhancers": ["wiki-plugin-myplugin"]
}
```
### Testing New Components
The architecture is designed to be testable:
```javascript
// Test your enhancer
const mockBaseHandler = { /* ... */ };
const enhancer = require('./my-authz-plugin')(log, loga, argv, mockBaseHandler);
// Test enhanced authorization
const req = { /* mock request */ };
const result = enhancer.isAuthorized(req, () => true);
```
### Potential Future Enhancements
Additional authorization enhancers could include:
- **`wiki-plugin-permissions`**: Fine-grained page/action permissions
- **`wiki-plugin-audit`**: Request logging and audit trails
- **`wiki-plugin-ip-filter`**: IP-based access control
- **`wiki-plugin-session-mgmt`**: Advanced session management
- **`wiki-plugin-2fa`**: Two-factor authentication
- **`wiki-plugin-delegation`**: Temporary access delegation
- **`wiki-plugin-webhooks`**: Webhook authentication
## Miscellaneous
### TODOs
- [x] Make `wiki-security-composable` use regular plugins instead of `wiki-authz` plugins for authorization enhancers.
- [x] Created `wiki-plugin-useraccesstokens` as replacement for `wiki-authz-useraccesstokens`.
- [x] Created `wiki-plugin-ratelimit` as replacement for `wiki-authz-ratelimit`.
- [ ] Manually test by running the wiki server with different configurations.
- [x] Test by itself. (Must fail if no auth provider is specified)
- [x] Test with `wiki-security-friends`.
- [x] Test with `wiki-security-passportjs`.
- [x] Test with `wiki-security-friends` and `wiki-plugin-useraccesstokens`.
- [x] Test with `wiki-security-passportjs` and `wiki-plugin-useraccesstokens`.
- [x] Test with `wiki-security-passportjs`, `wiki-plugin-useraccesstokens`, and `wiki-plugin-ratelimit`.
- [x] Make sure that everything in `wiki-plugin-useraccesstokens` has been implemented.
- [x] Add a README for `wiki-plugin-ratelimit`.
- [x] Make config for `wiki-plugin-ratelimit` flat like the others.
- [x] Provide explicit documentation on what the security handler interface does and how to use it, as documentation is lacking in `wiki-server`.
### Security Interface Method Usage Examples
The following examples show how each security interface method is used throughout the wiki-server codebase:
#### Owner Management
**`retrieveOwner(cb)` - Server Startup**
```javascript
// wiki-server/lib/server.js:920
// Called during server initialization to load owner information
securityhandler.retrieveOwner(e => {
// Throw if you can't find the initial owner
if (e) throw e
owner = securityhandler.getOwner()
console.log('owner: ' + owner)
app.emit('owner-set')
})
```
**`getOwner()` - Get Current Owner**
```javascript
// wiki-server/lib/server.js:923
// Used immediately after retrieveOwner to get the owner value
owner = securityhandler.getOwner()
```
**`setOwner(id, cb)` - Site Claiming**
```javascript
// wiki-security-friends/server/friends.coffee:109
// Called when a user claims an unclaimed site
setOwner id, (err) ->
if err
console.log 'Failed to claim wiki ', req.hostname, 'error ', err
res.sendStatus(500)
updateOwner getOwner
```
#### Authentication (User Identification)
**`getUser(req)` - Template Data**
```javascript
// wiki-server/lib/server.js:329, 365
// Used in route handlers to identify current user for template rendering
user = securityhandler.getUser(req)
const info = {
title,
pages: [],
authenticated: user ? true : false,
user: user,
seedNeighbors: argv.neighbors,
owned: owner ? true : false,
isOwner: securityhandler.isAuthorized(req) ? true : false,
ownedBy: owner ? owner : '',
}
```
#### Authorization (Access Control)
**`isAuthorized(req)` - Write Protection Middleware**
```javascript
// wiki-server/lib/server.js:474-480
// Middleware function to protect write operations
const authorized = (req, res, next) => {
if (securityhandler.isAuthorized(req)) {
next()
} else {
console.log('rejecting', req.path)
res.sendStatus(403)
}
}
```
**`isAuthorized(req)` - Applied to Protected Routes**
```javascript
// Examples from wiki-server/lib/server.js
app.post('/favicon.png', authorized, (req, res) => { /* ... */ })
app.get('/recycler/favicon.png', authorized, (req, res) => { /* ... */ })
app.get('/recycler/system/slugs.json', authorized, (req, res) => { /* ... */ })
app.get(/^\/recycler\/([a-z0-9-]+)\.json$/, authorized, (req, res) => { /* ... */ })
app.delete(/^\/recycler\/([a-z0-9-]+)\.json$/, authorized, (req, res) => { /* ... */ })
app.get('/proxy/*', authorized, (req, res) => { /* ... */ })
app.put(/^\/page\/([a-z0-9-]+)\/action$/i, authorized, (req, res) => { /* ... */ })
app.delete(/^\/([a-z0-9-]+)\.json$/, authorized, (req, res) => { /* ... */ })
```
**`isAdmin(req)` - Admin-Only Access**
```javascript
// wiki-server/lib/server.js:684-690
// Middleware function for admin-only operations
const admin = (req, res, next) => {
if (securityhandler.isAdmin(req)) {
next()
} else {
console.log('rejecting', req.path)
res.sendStatus(403)
}
}
// Applied to admin routes
app.get('/system/version.json', admin, (req, res) => {
// Return system version information
})
```
#### Route Definition
**`defineRoutes(app, cors, updateOwner)` - Security Plugin Setup**
```javascript
// wiki-server/lib/server.js:268
// Called during server initialization to register security routes
securityhandler.defineRoutes(app, cors, updateOwner)
```
**Example Route Definitions in Security Plugins:**
```javascript
// wiki-security-friends/server/friends.coffee:143-146
security.defineRoutes = (app, cors, updateOwner) ->
app.post '/login', cors, security.login(updateOwner)
app.get '/logout', cors, security.logout()
app.post '/auth/reclaim/', cors, security.reclaim()
```
#### Usage Pattern Analysis
1. **Startup Sequence**: `retrieveOwner()``getOwner()``defineRoutes()`
2. **Request Processing**: `getUser()` for identification → `isAuthorized()`/`isAdmin()` for access control
3. **Template Rendering**: Both `getUser()` and `isAuthorized()` used to populate template variables
4. **Protection Pattern**: Authorization methods wrapped in middleware functions for route protection
This usage pattern demonstrates clear separation between:
- **Authentication**: `getUser()` identifies who the user is
- **Authorization**: `isAuthorized()`/`isAdmin()` determine what they can do
- **Configuration**: Owner management and route definition handle setup
### Client Asset Management
One unique aspect of `wiki-security-composable` is how it handles client-side assets. The plugin **copies client assets** from the base authentication provider to its own `client/` directory during initialization. This might seem redundant since both packages are installed as dependencies, but it's necessary due to how the wiki server serves static assets.
#### Why Asset Copying is Required
The wiki server sets up a **single static route** for security assets:
```javascript
// wiki-server/lib/server.js:284
app.use('/security', express.static(path.join(argv.packageDir, argv.security_type, 'client'), staticPathOptions))
```
This creates a hardcoded expectation that security assets are located at:
```
{packageDir}/{security_type}/client/
```
When `security_type = "wiki-security-composable"`, the server expects assets at:
```
node_modules/wiki-security-composable/client/
```
However, the **base authentication provider's assets** (login forms, JavaScript, CSS) are actually located at:
```
node_modules/wiki-security-friends/client/
node_modules/wiki-security-passportjs/client/
```
#### The Path Resolution Problem
Unlike regular plugins, which each get their own static route (`/plugins/pluginname/`), **security plugins share a single `/security/` route**. The server cannot dynamically serve from different packages based on configuration because:
1. **Single Route Limitation**: Only one `app.use('/security', ...)` handler can exist
2. **Package Name Mismatch**: The route path is based on `argv.security_type`, not the base provider's name
3. **Client-Side Expectations**: The base provider's JavaScript expects its assets at `/security/`
#### Asset Copying Process
During initialization, `wiki-security-composable`:
1. **Detects** the base authentication provider's package directory
2. **Cleans** any existing client assets to avoid conflicts
3. **Copies** all client assets from the base provider to its own `client/` directory
4. **Preserves** the original file structure and permissions
```javascript
// Simplified version of the copying logic
const baseProviderClientPath = path.join(
path.dirname(require.resolve(`${authProvider}/package.json`)),
'client'
);
const ourClientPath = path.join(__dirname, '..', 'client');
// Clean and copy
fs.rmSync(ourClientPath, { recursive: true, force: true });
copyFiles(baseProviderClientPath, ourClientPath);
```
#### Alternative Approaches Considered
This copying approach could be avoided by:
1. **Multiple Static Routes**: Modifying wiki-server to serve from multiple security package paths
2. **Dynamic Route Resolution**: Making `/security/` redirect based on configuration
3. **Symlinks**: Using filesystem symlinks (but cross-platform compatibility issues)
4. **Route Proxying**: Having composable security proxy requests to appropriate packages
The **copying approach** was chosen because it:
- Maintains **full compatibility** with existing wiki-server code
- Provides **asset isolation** and predictable behavior
- Works reliably across different deployment scenarios
- Allows for **potential asset modification** if needed
#### Future Server Integration
If this composable architecture were adopted by the core wiki server, the asset copying would become unnecessary. The server could be modified to:
1. **Dynamically resolve** static routes based on the actual authentication provider
2. **Support multiple** security-related static routes
3. **Eliminate** the assumption that `security_type` equals the package containing client assets
This represents one of the key areas where the composable architecture has to work around existing server assumptions rather than requiring server modifications.

287
Server Changes Proposal.md Normal file
View File

@ -0,0 +1,287 @@
# Federated Wiki Security Architecture Enhancement
## Overview
This document proposes an enhancement to Federated Wiki's security architecture that separates **authentication** (who you are) from **authorization enhancement** (what you can do). This change would enable complementary security features like API tokens, rate limiting, and fine-grained permissions to work alongside any authentication method without conflicts.
## Current Architecture Limitations
### How Federated Wiki Security Works Today
Federated Wiki currently uses a single "security plugin" architecture where each plugin handles:
- **Authentication** - Proving user identity (OAuth, shared secrets, etc.)
- **Session management** - Storing user state
- **Authorization** - Determining what users can do
- **Route handling** - Login/logout endpoints
- **UI components** - Login dialogs and forms
### The Problem
This monolithic approach creates several issues:
1. **Mutually Exclusive Plugins**: You can only use one security plugin at a time
2. **Limited Extensibility**: Adding new authentication methods requires replacing the entire security system
3. **Feature Conflicts**: Plugins that want to enhance security (like API tokens) must either:
- Duplicate all existing authentication logic, or
- Use hacky workarounds to simulate existing authentication
### Real-World Impact
Consider these common scenarios that are difficult or impossible today:
```bash
# Impossible: OAuth login + API tokens
wiki --security_type passportjs --enable_api_tokens
# Impossible: Friends auth + rate limiting
wiki --security_type friends --enable_rate_limiting
# Impossible: Any auth + audit logging
wiki --security_type passportjs --enable_audit_log
```
Each security enhancement requires creating a completely new security plugin or forking existing ones.
## Proposed Solution: Composable Authentication Architecture
### Core Concept
Separate authentication from authorization enhancement:
- **Authentication Component**: Handles core identity verification (one plugin)
- **Authorization Enhancement Components**: Add complementary security features (multiple plugins)
### New Command Line Interface
*Proposed future syntax if server adopts this architecture natively:*
```bash
# Authentication (mutually exclusive)
wiki --auth_type friends
wiki --auth_type passportjs
wiki --auth_type ldap
# Authorization enhancements (complementary)
wiki --auth_type friends --authz_enhancers wiki-plugin-useraccesstokens
wiki --auth_type passportjs --authz_enhancers wiki-plugin-useraccesstokens,wiki-plugin-ratelimit
wiki --auth_type ldap --authz_enhancers wiki-plugin-useraccesstokens,wiki-plugin-permissions,wiki-plugin-audit
```
*Current implementation using `wiki-security-composable`:*
```bash
wiki --security_type composable --auth_provider wiki-security-friends --authz_enhancers wiki-plugin-useraccesstokens
```
### Plugin Types
**Authentication Plugins** (`wiki-auth-*`):
- Manage core user identity
- Handle owner/admin concepts
- Provide login/logout flows
- Store identity in consistent format
**Authorization Enhancement Plugins** (`wiki-plugin-*` with `securityEnhancer` export):
- Add supplementary authentication methods (API tokens, webhooks)
- Implement security policies (rate limiting, permissions)
- Provide monitoring/audit capabilities
- Work with any authentication plugin
## Technical Implementation
### Architecture Overview
```javascript
// Current: Single security handler
app.securityhandler = require(argv.security_type)(log, loga, argv)
// Proposed: Server-native composable architecture
const authHandler = require(argv.auth_provider)(log, loga, argv)
const enhancers = loadAuthzEnhancers(argv.authz_enhancers, authHandler)
app.securityhandler = composeSecurityHandler(authHandler, enhancers)
```
### Interface Contracts
**Authentication Plugin Interface**:
```javascript
{
retrieveOwner: (callback) => void,
getOwner: () => string,
setOwner: (identity, callback) => void,
getUser: (request) => object,
isAuthorized: (request) => boolean,
isAdmin: (request) => boolean,
defineRoutes: (app, cors, updateOwner) => void
}
```
**Authorization Enhancement Plugin Interface** (exported as `securityEnhancer` function):
```javascript
{
getUser?: (request) => object, // Optional: detect alternative auth
isAuthorized?: (request) => boolean, // Optional: additional auth checks
isAdmin?: (request) => boolean, // Optional: additional admin checks
defineRoutes?: (app, cors) => void // Optional: add routes
}
```
### Backward Compatibility
The current `--security_type` parameter continues to work:
```bash
# Old way (still works)
wiki --security_type friends
# Automatically translates to:
wiki --auth_type friends --authz_enhancers ""
```
Existing security plugins can be migrated incrementally without breaking changes.
### Migration Path
1. Extend `wiki-server` to support new parameter format
2. Implement composite security handler (as shown with `wiki-security-composable`)
3. Maintain full backward compatibility
## Current Implementation Status
This enhancement has a **working proof-of-concept implementation** in `wiki-security-composable` that demonstrates the architecture without requiring server changes. The implementation uses the existing plugin system where authorization enhancers are regular `wiki-plugin-*` packages that export a `securityEnhancer` function.
**Current Working Components:**
- `wiki-security-composable` - Foundation plugin that composes authentication with authorization enhancers
- `wiki-plugin-useraccesstokens` - Authorization enhancer for API tokens
- `wiki-plugin-ratelimit` - Authorization enhancer for request rate limiting
Additional authorization enhancers can be created following the established pattern
**How It Works Today:**
```bash
# Current implementation syntax
wiki --security_type composable --auth_provider wiki-security-friends --authz_enhancers wiki-plugin-useraccesstokens
```
The `wiki-security-composable` plugin acts as a wrapper that:
1. Loads the specified authentication provider (e.g., `wiki-security-friends`)
2. Loads authorization enhancer plugins that export `securityEnhancer` functions
3. Composes them using a function composition pattern
4. Presents a unified security interface to the wiki server
### Client asset management challenges with `wiki-security-composable`
When using `wiki-security-composable`, the server expects security assets to be in one specific location (e.g.,`wiki-security-composable/client/` when using the `--security_type composable` parameter). However, the actual authentication assets are in the base authentication provider's directory (e.g., `wiki-security-friends/client/`).
```javascript
// wiki-server/lib/server.js:284
app.use('/security', express.static(path.join(argv.packageDir, argv.security_type, 'client'), staticPathOptions))
```
This creates a hardcoded expectation that security assets are at `{packageDir}/{security_type}/client/`, but when using composable security:
- `security_type = "wiki-security-composable"`
- Actual authentication assets are in `wiki-security-friends/client/` or `wiki-security-passportjs/client/`
**Current Workaround**: `wiki-security-composable` must copy client assets from the base authentication provider to its own directory during initialization:
```javascript
// Detect base provider assets
const baseProviderClientPath = path.join(
path.dirname(require.resolve(`${authProvider}/package.json`)),
'client'
);
// Copy to our expected location
const ourClientPath = path.join(__dirname, '..', 'client');
fs.rmSync(ourClientPath, { recursive: true, force: true });
copyFiles(baseProviderClientPath, ourClientPath);
```
## Server Enhancement Proposal
The server could adopt this composable pattern natively to eliminate these workarounds and provide a cleaner developer and user experience:
### Native Configuration Support
Instead of the current workaround syntax, the server could support:
```bash
# Proposed native server support
wiki --auth_provider wiki-security-friends --authz_enhancers wiki-plugin-useraccesstokens,wiki-plugin-ratelimit
```
With backward compatibility:
```bash
# Current syntax continues to work
wiki --security_type friends # translates to --auth_provider wiki-security-friends
```
### Dynamic Asset Resolution
**Server Enhancement**: The server could dynamically resolve static routes based on the actual authentication provider:
```javascript
// Proposed server enhancement
const authProvider = argv.auth_provider;
const authAssetPath = path.join(path.dirname(require.resolve(`${authProvider}/package.json`)), 'client');
app.use('/auth', express.static(authAssetPath, staticPathOptions));
// Authorization enhancers continue using existing /plugins/ routes
```
### Built-in Composition Logic
**Server Enhancement**: The server could handle security composition natively:
```javascript
// Proposed server enhancement in defaultargs.js
if (argv.authz_enhancers) {
if (typeof argv.authz_enhancers === 'string') {
argv.authz_enhancers = argv.authz_enhancers.split(',').map(name => name.trim());
}
} else {
argv.authz_enhancers = [];
}
// Backward compatibility
if (argv.security_type && !argv.auth_provider) {
argv.auth_provider = argv.security_type;
}
```
```javascript
// Proposed server enhancement in server.js
const authHandler = require(argv.auth_provider)(log, loga, argv);
const authzHandlers = [];
if (argv.authz_enhancers) {
argv.authz_enhancers.forEach(pluginName => {
const plugin = require(pluginName);
if (plugin.securityEnhancer && typeof plugin.securityEnhancer === 'function') {
const handler = plugin.securityEnhancer(log, loga, argv, authHandler);
authzHandlers.push(handler);
}
});
}
// Create composite security handler
app.securityhandler = composeSecurityHandler(authHandler, authzHandlers);
```
### Authorization Enhancer Interface
The current plugin interface would remain unchanged:
```javascript
// wiki-plugin-useraccesstokens/index.js
export const securityEnhancer = (log, loga, argv, authHandler) => {
return {
getUser: (req, baseGetUser) => { /* enhanced user detection */ },
isAuthorized: (req, baseIsAuthorized) => { /* enhanced authorization */ },
isAdmin: (req, baseIsAdmin) => { /* enhanced admin checks */ },
middleware: (req, res, next) => { /* token validation, etc. */ },
defineRoutes: (app, cors, updateOwner) => { /* additional routes */ }
};
};
```

1021
client/dialog.css Normal file

File diff suppressed because it is too large Load Diff

10
client/relay.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<script>
function doPost(msg, origin) {
window.parent.postMessage(msg, origin);
}
</script>
</html>

213
client/security.js Normal file
View File

@ -0,0 +1,213 @@
/*
* Federated Wiki : Social Security Plugin
*
* Licensed under the MIT license.
* https://github.com/fedwiki/wiki-security-social/blob/master/LICENSE.txt
*/
/*
1. Display login button - if there is no authenticated user
2. Display logout button - if the user is authenticated
3. When user authenticated, claim site if unclaimed - and repaint footer.
*/
(function() {
var WinChan, claim_wiki, settings, setup, update_footer;
WinChan = require('./winchan.js');
settings = {};
claim_wiki = function() {
var myInit;
if (!isClaimed) {
myInit = {
method: 'GET',
cache: 'no-cache',
mode: 'same-origin',
credentials: 'include'
};
return fetch('/auth/claim-wiki', myInit).then(function(response) {
if (response.ok) {
return response.json().then(function(json) {
var ownerName;
if (wiki.lineup.bestTitle() === 'Login Required') {
return location.reload();
} else {
ownerName = json.ownerName;
window.isClaimed = true;
window.isOwner = true;
return update_footer(ownerName, true);
}
});
} else {
return console.log('Attempt to claim site failed', response);
}
});
}
};
update_footer = function(ownerName, isAuthenticated) {
var logoutIconClass, logoutTitle, signonTitle;
if (ownerName) {
$('footer > #site-owner').html("Site Owned by: <span id='site-owner' style='text-transform:capitalize;'>" + ownerName + "</span>");
}
$('footer > #security').empty();
if (isAuthenticated) {
if (isOwner) {
logoutTitle = "Sign-out";
logoutIconClass = 'fa fa-unlock fa-lg fa-fw';
} else {
logoutTitle = "Not Owner : Sign-out";
logoutIconClass = 'fa fa-lock fa-lg fa-fw notOwner';
}
$('footer > #security').append("<a href='#' id='logout' class='footer-item' title='" + logoutTitle + "'><i class='" + logoutIconClass + "'></i></a>");
$('footer > #security > #logout').on('click', function(e) {
var myInit;
e.preventDefault();
myInit = {
method: 'GET',
cache: 'no-cache',
mode: 'same-origin',
credentials: 'include'
};
return fetch('/logout', myInit).then(function(response) {
var user;
if (response.ok) {
window.isAuthenticated = false;
user = '';
document.cookie = "state=loggedOut" + ";domain=." + settings.cookieDomain + "; path=/; max-age=60; sameSite=Strict;";
return update_footer(ownerName, isAuthenticated);
} else {
return console.log('logout failed: ', response);
}
});
});
if (!isClaimed) {
$('footer > #security').append("<a href='#' id='claim' class='foot-item' title='Claim this Wiki'><i class='fa fa-key fa-lg fa-fw'></i></a>");
return $('footer > #security > #claim').on('click', function(e) {
e.preventDefault();
return claim_wiki();
});
}
} else {
if (!isClaimed) {
signonTitle = 'Claim this Wiki';
} else {
signonTitle = 'Wiki Owner Sign-on';
}
$('footer > #security').append("<a href='#' id='show-security-dialog' class='footer-item' title='" + signonTitle + "'><i class='fa fa-lock fa-lg fa-fw'></i></a>");
return $('footer > #security > #show-security-dialog').on('click', function(e) {
var w;
e.preventDefault();
document.cookie = ("wikiName=" + window.location.host) + (";domain=." + settings.cookieDomain + "; path=/; max-age=300; sameSite=Strict;");
return w = WinChan.open({
url: settings.dialogURL,
relay_url: settings.relayURL,
window_features: "menubar=0, location=0, resizable=0, scrollbars=1, status=0, dialog=1, width=700, height=375",
params: {}
}, function(err, r) {
if (err) {
return console.log(err);
} else {
window.isAuthenticated = true;
if (!isClaimed) {
return claim_wiki();
} else {
if (wiki.lineup.bestTitle() === 'Login Required') {
return location.reload();
} else {
return update_footer(ownerName, true);
}
}
}
});
});
}
};
setup = function(user) {
var lastCookie, myInit;
if (!$("link[href='https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css']").length) {
$('<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">').appendTo("head");
}
lastCookie = document.cookie;
window.setInterval(function() {
var currentCookie, myInit;
currentCookie = document.cookie;
if (currentCookie !== lastCookie) {
console.log("Cookie changed");
if (document.cookie.match('(?:^|;)\\s?state=(.*?)(?:;|$)') !== null) {
try {
switch (document.cookie.match('(?:^|;)\\s?state=(.*?)(?:;|$)')[1]) {
case 'loggedIn':
window.isAuthenticated = true;
break;
case 'loggedOut':
window.isAuthenticated = false;
}
myInit = {
method: 'GET',
cache: 'no-cache',
mode: 'same-origin'
};
fetch('/auth/client-settings.json', myInit).then(function(response) {
return response.json().then(function(json) {
window.isOwner = json.isOwner;
return update_footer(ownerName, isAuthenticated);
});
});
} catch (error) {}
}
return lastCookie = currentCookie;
}
}, 100);
if (!$("link[href='/security/style.css']").length) {
$('<link rel="stylesheet" href="/security/style.css">').appendTo("head");
}
myInit = {
method: 'GET',
cache: 'no-cache',
mode: 'same-origin'
};
return fetch('/auth/client-settings.json', myInit).then(function(response) {
if (response.ok) {
return response.json().then(function(json) {
var dialogHost, dialogProtocol;
window.isOwner = json.isOwner;
settings = json;
if (settings.wikiHost) {
dialogHost = settings.wikiHost;
} else {
dialogHost = window.location.hostname;
}
settings.cookieDomain = dialogHost;
if (settings.useHttps) {
dialogProtocol = 'https:';
} else {
dialogProtocol = window.location.protocol;
if (window.location.port) {
dialogHost = dialogHost + ':' + window.location.port;
}
}
settings.dialogURL = dialogProtocol + '//' + dialogHost + '/auth/loginDialog';
settings.relayURL = dialogProtocol + '//' + dialogHost + '/auth/relay.html';
settings.dialogAddAltURL = dialogProtocol + '//' + dialogHost + '/auth/addAuthDialog';
return update_footer(ownerName, isAuthenticated);
});
} else {
return console.log('Unable to fetch client settings: ', response);
}
});
};
window.plugins.security = {
setup: setup,
claim_wiki: claim_wiki,
update_footer: update_footer
};
}).call(this);

13
client/style.css Normal file
View File

@ -0,0 +1,13 @@
#security #show-security-dialog, #logout, #claim{
color: gold;
}
#security #addAltAuth, #claim {
color: floralwhite;
}
.notOwner {
transform: rotate(20deg);
}

301
client/winchan.js Normal file
View File

@ -0,0 +1,301 @@
var WinChan = (function() {
var RELAY_FRAME_NAME = "__winchan_relay_frame";
var CLOSE_CMD = "die";
// a portable addListener implementation
function addListener(w, event, cb) {
if(w.attachEvent) w.attachEvent('on' + event, cb);
else if (w.addEventListener) w.addEventListener(event, cb, false);
}
// a portable removeListener implementation
function removeListener(w, event, cb) {
if(w.detachEvent) w.detachEvent('on' + event, cb);
else if (w.removeEventListener) w.removeEventListener(event, cb, false);
}
// checking for IE8 or above
function isInternetExplorer() {
var rv = -1; // Return value assumes failure.
var ua = navigator.userAgent;
if (navigator.appName === 'Microsoft Internet Explorer') {
var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
if (re.exec(ua) != null)
rv = parseFloat(RegExp.$1);
}
// IE > 11
else if (ua.indexOf("Trident") > -1) {
var re = new RegExp("rv:([0-9]{2,2}[\.0-9]{0,})");
if (re.exec(ua) !== null) {
rv = parseFloat(RegExp.$1);
}
}
return rv >= 8;
}
// checking Mobile Firefox (Fennec)
function isFennec() {
try {
// We must check for both XUL and Java versions of Fennec. Both have
// distinct UA strings.
var userAgent = navigator.userAgent;
return (userAgent.indexOf('Fennec/') != -1) || // XUL
(userAgent.indexOf('Firefox/') != -1 && userAgent.indexOf('Android') != -1); // Java
} catch(e) {}
return false;
}
// feature checking to see if this platform is supported at all
function isSupported() {
return (window.JSON && window.JSON.stringify &&
window.JSON.parse && window.postMessage);
}
// given a URL, extract the origin. Taken from: https://github.com/firebase/firebase-simple-login/blob/d2cb95b9f812d8488bdbfba51c3a7c153ba1a074/js/src/simple-login/transports/WinChan.js#L25-L30
function extractOrigin(url) {
if (!/^https?:\/\//.test(url)) url = window.location.href;
var m = /^(https?:\/\/[\-_a-zA-Z\.0-9:]+)/.exec(url);
if (m) return m[1];
return url;
}
// find the relay iframe in the opener
function findRelay() {
var loc = window.location;
var frames = window.opener.frames;
for (var i = frames.length - 1; i >= 0; i--) {
try {
if (frames[i].location.protocol === window.location.protocol &&
frames[i].location.host === window.location.host &&
frames[i].name === RELAY_FRAME_NAME)
{
return frames[i];
}
} catch(e) { }
}
return;
}
var isIE = isInternetExplorer();
if (isSupported()) {
/* General flow:
* 0. user clicks
* (IE SPECIFIC) 1. caller adds relay iframe (served from trusted domain) to DOM
* 2. caller opens window (with content from trusted domain)
* 3. window on opening adds a listener to 'message'
* (IE SPECIFIC) 4. window on opening finds iframe
* 5. window checks if iframe is "loaded" - has a 'doPost' function yet
* (IE SPECIFIC5) 5a. if iframe.doPost exists, window uses it to send ready event to caller
* (IE SPECIFIC5) 5b. if iframe.doPost doesn't exist, window waits for frame ready
* (IE SPECIFIC5) 5bi. once ready, window calls iframe.doPost to send ready event
* 6. caller upon reciept of 'ready', sends args
*/
return {
open: function(opts, cb) {
if (!cb) throw "missing required callback argument";
// test required options
var err;
if (!opts.url) err = "missing required 'url' parameter";
if (!opts.relay_url) err = "missing required 'relay_url' parameter";
if (err) setTimeout(function() { cb(err); }, 0);
// supply default options
if (!opts.window_name) opts.window_name = null;
if (!opts.window_features || isFennec()) opts.window_features = undefined;
// opts.params may be undefined
var iframe;
// sanity check, are url and relay_url the same origin?
var origin = extractOrigin(opts.url);
if (origin !== extractOrigin(opts.relay_url)) {
return setTimeout(function() {
cb('invalid arguments: origin of url and relay_url must match');
}, 0);
}
var messageTarget;
if (isIE) {
// first we need to add a "relay" iframe to the document that's served
// from the target domain. We can postmessage into a iframe, but not a
// window
iframe = document.createElement("iframe");
// iframe.setAttribute('name', framename);
iframe.setAttribute('src', opts.relay_url);
iframe.style.display = "none";
iframe.setAttribute('name', RELAY_FRAME_NAME);
document.body.appendChild(iframe);
messageTarget = iframe.contentWindow;
}
var w = opts.popup || window.open(opts.url, opts.window_name, opts.window_features);
if (opts.popup) {
w.location.href = opts.url;
}
if (!messageTarget) messageTarget = w;
// lets listen in case the window blows up before telling us
var closeInterval = setInterval(function() {
if (w && w.closed) {
cleanup();
if (cb) {
cb('User closed the popup window');
cb = null;
}
}
}, 500);
var req = JSON.stringify({a: 'request', d: opts.params});
// cleanup on unload
function cleanup() {
if (iframe) document.body.removeChild(iframe);
iframe = undefined;
if (closeInterval) closeInterval = clearInterval(closeInterval);
removeListener(window, 'message', onMessage);
removeListener(window, 'unload', cleanup);
if (w) {
try {
w.close();
} catch (securityViolation) {
// This happens in Opera 12 sometimes
// see https://github.com/mozilla/browserid/issues/1844
messageTarget.postMessage(CLOSE_CMD, origin);
}
}
w = messageTarget = undefined;
}
addListener(window, 'unload', cleanup);
function onMessage(e) {
if (e.origin !== origin) { return; }
try {
var d = JSON.parse(e.data);
if (d.a === 'ready') messageTarget.postMessage(req, origin);
else if (d.a === 'error') {
cleanup();
if (cb) {
cb(d.d);
cb = null;
}
} else if (d.a === 'response') {
cleanup();
if (cb) {
cb(null, d.d);
cb = null;
}
}
} catch(err) { }
}
addListener(window, 'message', onMessage);
return {
close: cleanup,
focus: function() {
if (w) {
try {
w.focus();
} catch (e) {
// IE7 blows up here, do nothing
}
}
}
};
},
onOpen: function(cb) {
var o = "*";
var msgTarget = isIE ? findRelay() : window.opener;
if (!msgTarget) throw "can't find relay frame";
function doPost(msg) {
msg = JSON.stringify(msg);
if (isIE) msgTarget.doPost(msg, o);
else msgTarget.postMessage(msg, o);
}
function onMessage(e) {
// only one message gets through, but let's make sure it's actually
// the message we're looking for (other code may be using
// postmessage) - we do this by ensuring the payload can
// be parsed, and it's got an 'a' (action) value of 'request'.
var d;
try {
d = JSON.parse(e.data);
} catch(err) { }
if (!d || d.a !== 'request') return;
removeListener(window, 'message', onMessage);
o = e.origin;
if (cb) {
// this setTimeout is critically important for IE8 -
// in ie8 sometimes addListener for 'message' can synchronously
// cause your callback to be invoked. awesome.
setTimeout(function() {
cb(o, d.d, function(r) {
cb = undefined;
doPost({a: 'response', d: r});
});
}, 0);
}
}
function onDie(e) {
if (e.data === CLOSE_CMD) {
try { window.close(); } catch (o_O) {}
}
}
addListener(isIE ? msgTarget : window, 'message', onMessage);
addListener(isIE ? msgTarget : window, 'message', onDie);
// we cannot post to our parent that we're ready before the iframe
// is loaded. (IE specific possible failure)
try {
doPost({a: "ready"});
} catch(e) {
// this code should never be exectued outside IE
addListener(msgTarget, 'load', function(e) {
doPost({a: "ready"});
});
}
// if window is unloaded and the client hasn't called cb, it's an error
var onUnload = function() {
try {
// IE8 doesn't like this...
removeListener(isIE ? msgTarget : window, 'message', onDie);
} catch (ohWell) { }
if (cb) doPost({ a: 'error', d: 'client closed window' });
cb = undefined;
// explicitly close the window, in case the client is trying to reload or nav
try { window.close(); } catch (e) { }
};
addListener(window, 'unload', onUnload);
return {
detach: function() {
removeListener(window, 'unload', onUnload);
}
};
}
};
} else {
return {
open: function(url, winopts, arg, cb) {
setTimeout(function() { cb("unsupported browser"); }, 0);
},
onOpen: function(cb) {
setTimeout(function() { cb("unsupported browser"); }, 0);
}
};
}
})();
if (typeof module !== 'undefined' && module.exports) {
module.exports = WinChan;
}

3
index.js Normal file
View File

@ -0,0 +1,3 @@
// **index.js**
// Entry point for the composable security plugin
module.exports = require('./lib/composable-security');

267
lib/composable-security.js Normal file
View 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;
};

769
package-lock.json generated Normal file
View File

@ -0,0 +1,769 @@
{
"name": "wiki-security-composable",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wiki-security-composable",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"coffeescript": "^1.12.7",
"glob": "^11.0.3",
"lodash": "^4.17.21"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"wiki-security-friends": "*",
"wiki-security-passportjs": "^0.11.2"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@passport-js/passport-twitter": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@passport-js/passport-twitter/-/passport-twitter-1.0.10.tgz",
"integrity": "sha512-hirIwjs29OIw8Gt/unt7QyjbpekM/JwmjK8jlzhlpxBOCCa7MtGYMwQY6t2bhGevuXrxqORR2s4ShN78Sn4OMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@passport-js/xtraverse": "^0.1.4",
"passport-oauth1": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/passportjs"
}
},
"node_modules/@passport-js/xtraverse": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@passport-js/xtraverse/-/xtraverse-0.1.4.tgz",
"integrity": "sha512-onVnYnmOQ/wd3nWeKh109j2Vbb4MHBFhD6ewHx4rnmGzjqhQAdGu8oEw7Rm+/5zHT/9o54ClSeFfJHZl9WLvBw==",
"peer": true,
"dependencies": {
"@xmldom/xmldom": "^0.9.3"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.9.8",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.8.tgz",
"integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=14.6"
}
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/coffeescript": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.12.7.tgz",
"integrity": "sha512-pLXHFxQMPklVoEekowk8b3erNynC+DVJzChxS/LCBBgR6/8AJkHivkm//zbowcfc7BTCAjryuhx6gPqPRfsFoA==",
"license": "MIT",
"bin": {
"cake": "bin/cake",
"coffee": "bin/coffee"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT"
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.0.3",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/jackspeak": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
"license": "ISC",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/minimatch": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"license": "ISC",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/oauth": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
"license": "MIT",
"peer": true
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
"node_modules/passport": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.3.2.tgz",
"integrity": "sha512-aqgxMQxuRz79M4LVo8fl3/bsh6Ozcb34G8MVDs7Oavy88ROLSVvTgYoWnX3TpxdQg66HiXvpb+lcuFPnDrmiOA==",
"license": "MIT",
"peer": true,
"dependencies": {
"passport-strategy": "1.x.x",
"pause": "0.0.1"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/passport-github2": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz",
"integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==",
"peer": true,
"dependencies": {
"passport-oauth2": "1.x.x"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/passport-google-oauth20": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
"integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"passport-oauth2": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/passport-oauth1": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/passport-oauth1/-/passport-oauth1-1.3.0.tgz",
"integrity": "sha512-8T/nX4gwKTw0PjxP1xfD0QhrydQNakzeOpZ6M5Uqdgz9/a/Ag62RmJxnZQ4LkbdXGrRehQHIAHNAu11rCP46Sw==",
"license": "MIT",
"peer": true,
"dependencies": {
"oauth": "0.9.x",
"passport-strategy": "1.x.x",
"utils-merge": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-oauth2": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
"integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
"license": "MIT",
"peer": true,
"dependencies": {
"base64url": "3.x.x",
"oauth": "0.10.x",
"passport-strategy": "1.x.x",
"uid2": "0.0.x",
"utils-merge": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-oauth2/node_modules/oauth": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
"integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
"license": "MIT",
"peer": true
},
"node_modules/passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
"peer": true,
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==",
"peer": true
},
"node_modules/seedrandom": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
"license": "MIT",
"peer": true
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/uid2": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
"license": "MIT",
"peer": true
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wiki-security-friends": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/wiki-security-friends/-/wiki-security-friends-0.2.6.tgz",
"integrity": "sha512-f8QWeo5HzqGc16XMEQSgbL7MzTcCtxcJuv794Fg3pi4wkLA8qBispeS04VJQfhZbnWvuAg48rM0RAkwVETbT/Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"coffeescript": "^2.4.1",
"seedrandom": "^3.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/wiki-security-friends/node_modules/coffeescript": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.7.0.tgz",
"integrity": "sha512-hzWp6TUE2d/jCcN67LrW1eh5b/rSDKQK6oD6VMLlggYVUUFexgTH9z3dNYihzX4RMhze5FTUsUmOXViJKFQR/A==",
"license": "MIT",
"peer": true,
"bin": {
"cake": "bin/cake",
"coffee": "bin/coffee"
},
"engines": {
"node": ">=6"
}
},
"node_modules/wiki-security-passportjs": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/wiki-security-passportjs/-/wiki-security-passportjs-0.11.2.tgz",
"integrity": "sha512-NLay7hMPVLPp52pPNaSW6N85G89qVXfeiq7QBMp1E/ll6xupHVhx9qWDa5l6U3e4+/bqQbX6leNRg1ZUYiJRNg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@passport-js/passport-twitter": "^1.0.8",
"coffeescript": "^2.4.1",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.19",
"passport": "^0.3.2",
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0",
"passport-oauth2": "^1.6.1"
}
},
"node_modules/wiki-security-passportjs/node_modules/coffeescript": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.7.0.tgz",
"integrity": "sha512-hzWp6TUE2d/jCcN67LrW1eh5b/rSDKQK6oD6VMLlggYVUUFexgTH9z3dNYihzX4RMhze5FTUsUmOXViJKFQR/A==",
"license": "MIT",
"peer": true,
"bin": {
"cake": "bin/cake",
"coffee": "bin/coffee"
},
"engines": {
"node": ">=6"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
}
}
}

35
package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "wiki-security-composable",
"version": "0.1.0",
"description": "Composable security plugin foundation for Federated Wiki",
"main": "index.js",
"keywords": [
"wiki",
"fedwiki",
"security",
"authentication",
"authorization",
"composable"
],
"author": "Christian Galo",
"license": "MIT",
"dependencies": {
"coffeescript": "^1.12.7",
"glob": "^11.0.3",
"lodash": "^4.17.21"
},
"scripts": {
"test": "node test/composable-security.test.js"
},
"peerDependencies": {
"wiki-security-friends": "*",
"wiki-security-passportjs": "^0.11.2"
},
"engines": {
"node": ">=14.0.0"
},
"repository": {
"type": "git",
"url": "https://git.coopcloud.tech/wiki-cafe/wiki-security-composable.git"
}
}

42
test/README.md Normal file
View File

@ -0,0 +1,42 @@
# Test Files
This directory contains tests for the composable security architecture.
## Test Files
### `composable-security.test.js`
**Unit/Mock Test** - Tests the composable security architecture using completely mocked components. This test should always pass as it doesn't depend on external modules.
- **Purpose**: Validate the composition logic and security interface compatibility
- **Dependencies**: None (uses mocks)
- **Usage**: `node test/composable-security.test.js` or `node composable-security.test.js` (from test dir)
### `integration.test.js`
**Integration Test** - Tests the composable security system with real authorization enhancer modules. This test will fail if the required modules are not installed.
- **Purpose**: Validate that the system works with actual authorization enhancers
- **Dependencies**: Requires `wiki-plugin-useraccesstokens` and `wiki-plugin-ratelimit` to be installed
- **Usage**: `node test/integration.test.js`
## Running Tests
```bash
# Run the mock test (should always pass)
npm test # or node test/composable-security.test.js
# Run the integration test (requires real modules)
node test/integration.test.js
```
## Test Structure
Both tests validate:
- Module loading and configuration parsing
- Route registration from base handler and enhancers
- Owner management functionality
- Security interface methods (`getUser`, `isAuthorized`, `isAdmin`)
- Request handling with different authentication scenarios
The main difference is that the integration test uses real modules while the unit test uses mocks.

View File

@ -0,0 +1,314 @@
#!/usr/bin/env node
/**
* Test script to demonstrate the composable security architecture
*
* This script simulates loading and using the composable security system
* with different configurations. It can be run from either the project root
* or from within the test/ directory.
*
* Usage:
* node test/composable-security.test.js (from project root)
* node composable-security.test.js (from test/ directory)
*/
const fs = require('fs');
const path = require('path');
// Mock the require function first, before any other requires
const originalRequire = require;
// Mock base security plugin (friends)
const mockFriendsPlugin = (log, loga, argv) => ({
retrieveOwner: (cb) => {
console.log('[FRIENDS] Retrieving owner...');
cb();
},
getOwner: () => 'test-owner',
setOwner: (id, cb) => {
console.log(`[FRIENDS] Setting owner to: ${id}`);
cb();
},
getUser: (req) => {
if (req.session && req.session.friend) {
return 'test-user';
}
return '';
},
isAuthorized: (req) => {
return req.session && req.session.friend === 'test-secret';
},
isAdmin: (req) => {
return req.session && req.session.friend === 'admin-secret';
},
defineRoutes: (app, cors, updateOwner) => {
app.post('/login', 'friends-login-handler');
app.get('/logout', 'friends-logout-handler');
},
_debug: { authProvider: 'wiki-security-friends' }
});
// Mock plugin with security enhancer (new approach)
const mockPluginWithSecurityEnhancer = {
securityEnhancer: (log, loga, argv, baseHandler) => ({
isAuthorized: (req, baseIsAuthorized) => {
console.log('[PLUGIN-ENHANCER] Checking authorization');
return baseIsAuthorized();
},
getUser: (req, baseGetUser) => {
if (req.tokenAuth && req.tokenAuth.user) {
console.log('[PLUGIN-ENHANCER] Returning token user');
return req.tokenAuth.user;
}
return baseGetUser();
},
middleware: (req, res, next) => {
console.log('[PLUGIN-ENHANCER] Middleware processing request');
// Simulate token validation
if (req.headers.authorization === 'Bearer test-token') {
req.tokenAuth = { user: 'token-user' };
}
next();
},
defineRoutes: (app, cors, updateOwner) => {
console.log('[PLUGIN-ENHANCER] Defining routes');
app.get('/plugin/test/tokens', 'token-management-handler');
}
}),
startServer: (params) => {
console.log('[PLUGIN] Regular plugin server started');
}
};
// Mock authz enhancer (simplified)
const mockTokenEnhancer = (log, loga, argv, baseHandler) => ({
middleware: (req, res, next) => {
console.log('[TOKENS] Processing token middleware');
next();
},
getUser: (req, baseGetUser) => {
const baseUser = baseGetUser();
if (baseUser) return baseUser;
// Check for token auth
if (req.headers && req.headers.authorization) {
console.log('[TOKENS] Token authentication detected');
return 'token-user';
}
return '';
},
defineRoutes: (app, cors, updateOwner) => {
app.post('/auth/tokens', 'create-token-handler');
app.get('/auth/tokens', 'list-tokens-handler');
}
});
const mockRateLimitEnhancer = (log, loga, argv, baseHandler) => ({
middleware: (req, res, next) => {
console.log('[RATELIMIT] Processing rate limit middleware');
next();
},
defineRoutes: (app, cors, updateOwner) => {
app.get('/auth/ratelimit/status', 'ratelimit-status-handler');
}
});
// Set up module mocking
require = function(module) {
switch(module) {
case 'wiki-security-friends':
return mockFriendsPlugin;
case 'wiki-plugin-useraccesstokens':
return mockPluginWithSecurityEnhancer;
case 'wiki-plugin-ratelimit':
return mockPluginWithSecurityEnhancer; // Same structure for testing
default:
return originalRequire(module);
}
};
// Mock dependencies that would normally be provided by the wiki server
const mockLog = (msg) => console.log(`[LOG] ${msg}`);
const mockLoga = mockLog;
// Support running from different directories
let statusPath = '/tmp/fedwiki-test-status';
let indexPath = '../index.js'; // Default: relative to test directory
// Check if we're running from the root directory
const cwd = process.cwd();
const testDir = __dirname;
if (fs.existsSync(path.join(cwd, 'index.js'))) {
// Running from project root - use relative path to current working directory
statusPath = './test-status';
indexPath = path.join(cwd, 'index.js');
} else {
// Running from test directory or elsewhere - use relative to test file
statusPath = './test-status';
indexPath = '../index.js';
}
const mockArgv = {
status: statusPath,
auth_provider: mockFriendsPlugin,
authz_enhancers: [mockPluginWithSecurityEnhancer, mockPluginWithSecurityEnhancer], // Two mock plugins
ratelimit_config: {
windowMs: 60000, // 1 minute for testing
maxRequests: 10,
maxAuthRequests: 3
}
};
// Mock Express app
const mockApp = {
use: (path, middleware) => {
if (typeof path === 'function') {
console.log(`[APP] Added global middleware`);
} else {
console.log(`[APP] Added middleware for: ${path || 'all routes'}`);
}
},
get: (path, ...handlers) => console.log(`[APP] Added GET route: ${path}`),
post: (path, ...handlers) => console.log(`[APP] Added POST route: ${path}`),
delete: (path, ...handlers) => console.log(`[APP] Added DELETE route: ${path}`)
};
const mockCors = (req, res, next) => next();
const mockUpdateOwner = (owner) => console.log(`[OWNER] Updated to: ${owner}`);
console.log('=== Composable Security Architecture Test ===\n');
console.log(`Running from: ${process.cwd()}`);
console.log(`Using index path: ${indexPath}`);
console.log(`Using status path: ${statusPath}\n`);
// Test 1: Load the composable security system
console.log('1. Loading composable security system...');
try {
const ComposableSecurity = require(indexPath);
const security = ComposableSecurity(mockLog, mockLoga, mockArgv);
console.log('✓ Composable security loaded successfully');
console.log(`✓ Debug info:`, security._debug);
console.log();
// Test 2: Initialize routes
console.log('2. Initializing routes...');
security.defineRoutes(mockApp, mockCors, mockUpdateOwner);
console.log('✓ Routes initialized');
console.log();
// Test 3: Test owner management
console.log('3. Testing owner management...');
security.retrieveOwner(() => {
const owner = security.getOwner();
console.log(`✓ Current owner: ${owner}`);
});
console.log();
// Test 4: Test request handling
console.log('4. Testing request handling...');
// Mock requests
const mockReqAuth = {
session: { friend: 'test-secret' },
ip: '127.0.0.1',
path: '/test',
get: () => null,
query: {}
};
const mockReqToken = {
session: {},
ip: '127.0.0.1',
path: '/api/test',
headers: { authorization: 'Bearer uat_test123' },
get: (header) => header === 'Authorization' ? 'Bearer uat_test123' : null,
query: {}
};
const mockReqUnauth = {
session: {},
ip: '127.0.0.1',
path: '/test',
get: () => null,
query: {}
};
// Test authenticated request
console.log('Testing authenticated request...');
console.log(` User: ${security.getUser(mockReqAuth)}`);
console.log(` Authorized: ${security.isAuthorized(mockReqAuth)}`);
console.log(` Admin: ${security.isAdmin(mockReqAuth)}`);
// Test token request
console.log('Testing token request...');
console.log(` User: ${security.getUser(mockReqToken)}`);
console.log(` Authorized: ${security.isAuthorized(mockReqToken)}`);
// Test unauthenticated request
console.log('Testing unauthenticated request...');
console.log(` User: ${security.getUser(mockReqUnauth)}`);
console.log(` Authorized: ${security.isAuthorized(mockReqUnauth)}`);
console.log('✓ Request handling tests completed');
console.log();
console.log('=== Test Summary ===');
console.log('✓ All tests completed successfully');
console.log('✓ Composable security architecture is working');
console.log('✓ Auth provider and authz enhancers loaded correctly');
console.log('✓ Security interface maintained compatibility');
} catch (error) {
console.error('✗ Test failed:', error.message);
console.error(error.stack);
process.exit(1);
}
// Test 5: Plugin-based enhancers
console.log('\n5. Testing plugin-based enhancers...');
try {
const mockArgvPlugin = {
status: statusPath,
auth_provider: 'wiki-security-friends',
authz_enhancers: ['wiki-plugin-useraccesstokens', 'wiki-plugin-ratelimit']
};
const ComposableSecurity = require(indexPath);
const securityPlugin = ComposableSecurity(mockLog, mockLoga, mockArgvPlugin);
console.log('✓ Plugin-based security loaded successfully');
console.log(`✓ Loaded enhancers: ${securityPlugin._debug.loadedEnhancers.join(', ')}`);
// Test routes initialization
console.log('Initializing plugin-based routes...');
securityPlugin.defineRoutes(mockApp, mockCors, mockUpdateOwner);
// Test token request with plugin enhancer
const mockReqPluginToken = {
session: {},
ip: '127.0.0.1',
path: '/api/test',
headers: { authorization: 'Bearer test-token' },
get: (header) => header === 'Authorization' ? 'Bearer test-token' : null,
query: {}
};
console.log('Testing plugin token request...');
console.log(` User: ${securityPlugin.getUser(mockReqPluginToken)}`);
console.log(` Authorized: ${securityPlugin.isAuthorized(mockReqPluginToken)}`);
console.log('✓ Plugin-based enhancer tests completed');
console.log();
} catch (error) {
console.error('✗ Plugin-based test failed:', error.message);
console.error(error.stack);
process.exit(1);
}
console.log('=== Final Test Summary ===');
console.log('✓ All tests completed successfully');
console.log('✓ Plugin-based enhancers working correctly');
console.log('✓ Composable security architecture is fully functional');

175
test/integration.test.js Normal file
View File

@ -0,0 +1,175 @@
#!/usr/bin/env node
/**
* Integration Test for Composable Security Architecture
*
* This test attempts to load the composable security system with real
* plugin-based authorization enhancers. It will only pass if the required
* modules are properly installed and configured.
*
* Required modules for full test:
* - wiki-security-friends (mocked in this test)
* - wiki-plugin-useraccesstokens
* - wiki-plugin-ratelimit
*
* Usage:
* node test/integration.test.js
*/
const path = require('path');
// Mock the require function first, before any other requires
const originalRequire = require;
// Mock base security plugin (friends)
const mockFriendsPlugin = (log, loga, argv) => ({
retrieveOwner: (cb) => {
console.log('[FRIENDS] Retrieving owner...');
cb();
},
getOwner: () => 'test-owner',
setOwner: (id, cb) => {
console.log(`[FRIENDS] Setting owner to: ${id}`);
cb();
},
getUser: (req) => {
if (req.session && req.session.friend) {
return 'test-user';
}
return '';
},
isAuthorized: (req) => {
return req.session && req.session.friend === 'test-secret';
},
isAdmin: (req) => {
return req.session && req.session.friend === 'admin-secret';
},
defineRoutes: (app, cors, updateOwner) => {
app.post('/login', 'friends-login-handler');
app.get('/logout', 'friends-logout-handler');
},
_debug: { authProvider: 'wiki-security-friends' }
});
// Set up module mocking - only mock the base auth provider
// The plugin-based authorization enhancers will be loaded as real modules
const moduleCache = new Map();
moduleCache.set('wiki-security-friends', mockFriendsPlugin);
require = function(module) {
if (moduleCache.has(module)) {
return moduleCache.get(module);
}
return originalRequire(module);
};
const mockLog = (msg) => console.log(`[LOG] ${msg}`);
const mockLoga = mockLog;
const mockArgv = {
status: './test-status',
auth_provider: 'wiki-security-friends',
authz_enhancers: ['wiki-plugin-useraccesstokens', 'wiki-plugin-ratelimit'],
ratelimit_config: {
windowMs: 60000, // 1 minute for testing
maxRequests: 10,
maxAuthRequests: 3
}
};
// Mock Express app
const mockApp = {
use: (path, middleware) => console.log(`[APP] Added middleware for: ${path || 'all routes'}`),
get: (path, ...handlers) => console.log(`[APP] Added GET route: ${path}`),
post: (path, ...handlers) => console.log(`[APP] Added POST route: ${path}`),
delete: (path, ...handlers) => console.log(`[APP] Added DELETE route: ${path}`)
};
const mockCors = (req, res, next) => next();
const mockUpdateOwner = (owner) => console.log(`[OWNER] Updated to: ${owner}`);
console.log('=== Composable Security Integration Test ===\n');
console.log('This test attempts to load real plugin-based authorization enhancers.');
console.log('It will fail if required plugin modules are not installed.\n');
// Test 1: Load the composable security system
console.log('1. Loading composable security system...');
try {
const ComposableSecurity = require('../index.js');
const security = ComposableSecurity(mockLog, mockLoga, mockArgv);
console.log('✓ Composable security loaded successfully');
console.log(`✓ Debug info:`, security._debug);
console.log();
// Test 2: Initialize routes
console.log('2. Initializing routes...');
security.defineRoutes(mockApp, mockCors, mockUpdateOwner);
console.log('✓ Routes initialized');
console.log();
// Test 3: Test owner management
console.log('3. Testing owner management...');
security.retrieveOwner(() => {
const owner = security.getOwner();
console.log(`✓ Current owner: ${owner}`);
});
console.log();
// Test 4: Test request handling
console.log('4. Testing request handling...');
// Mock requests
const mockReqAuth = {
session: { friend: 'test-secret' },
ip: '127.0.0.1',
path: '/test',
get: () => null,
query: {}
};
const mockReqToken = {
session: {},
ip: '127.0.0.1',
path: '/api/test',
get: (header) => header === 'Authorization' ? 'Bearer uat_test123' : null,
query: {}
};
const mockReqUnauth = {
session: {},
ip: '127.0.0.1',
path: '/test',
get: () => null,
query: {}
};
// Test authenticated request
console.log('Testing authenticated request...');
console.log(` User: ${security.getUser(mockReqAuth)}`);
console.log(` Authorized: ${security.isAuthorized(mockReqAuth)}`);
console.log(` Admin: ${security.isAdmin(mockReqAuth)}`);
// Test token request (will fail without real token validation)
console.log('Testing token request...');
console.log(` User: ${security.getUser(mockReqToken)}`);
console.log(` Authorized: ${security.isAuthorized(mockReqToken)}`);
// Test unauthenticated request
console.log('Testing unauthenticated request...');
console.log(` User: ${security.getUser(mockReqUnauth)}`);
console.log(` Authorized: ${security.isAuthorized(mockReqUnauth)}`);
console.log('✓ Request handling tests completed');
console.log();
console.log('=== Integration Test Summary ===');
console.log('✓ All integration tests completed successfully');
console.log('✓ Composable security works with real plugin-based authorization enhancers');
console.log('✓ Auth provider and plugin enhancers loaded correctly');
console.log('✓ Security interface maintained compatibility');
} catch (error) {
console.error('✗ Integration test failed:', error.message);
console.error('\nThis is expected if the required plugin modules are not installed.');
console.error('To run a successful test with mocked modules, use: node test/composable-security.test.js');
process.exit(1);
}