Initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
/client
|
506
README.md
Normal file
506
README.md
Normal 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
287
Server Changes Proposal.md
Normal 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
1021
client/dialog.css
Normal file
File diff suppressed because it is too large
Load Diff
10
client/relay.html
Normal file
10
client/relay.html
Normal 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
213
client/security.js
Normal 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
13
client/style.css
Normal 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
301
client/winchan.js
Normal 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
3
index.js
Normal 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
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;
|
||||
};
|
769
package-lock.json
generated
Normal file
769
package-lock.json
generated
Normal 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
35
package.json
Normal 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
42
test/README.md
Normal 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.
|
314
test/composable-security.test.js
Normal file
314
test/composable-security.test.js
Normal 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
175
test/integration.test.js
Normal 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);
|
||||
}
|
Reference in New Issue
Block a user