Files
igny8/docs/plans/implemented/ACCURATE-INTEGRATION-SECURITY-PLAN.md

298 lines
9.3 KiB
Markdown

# IGNY8 ↔ WordPress Integration Security Plan
**Date:** 2026-01-13
**Status:** Audit Complete - Plan Ready
---
## PART 1: ACTUAL ARCHITECTURE (VERIFIED)
### 1.1 Publishing Flow (WORKING - No Changes Needed)
```
Frontend (Review/Approved pages)
POST /v1/publisher/publish/
PublisherService._publish_to_destination()
├── Gets API key: content.site.wp_api_key (SINGLE SOURCE OF TRUTH)
├── Gets URL: content.site.domain or content.site.url
WordPressAdapter._publish_via_api_key()
POST {site_url}/wp-json/igny8/v1/publish
Headers: X-IGNY8-API-KEY: {api_key}
Plugin: check_permission() validates header against stored igny8_api_key
Plugin creates/updates WordPress post
```
**Key Finding:** Publishing does NOT use SiteIntegration model. It uses:
- `Site.wp_api_key` - for authentication
- `Site.domain` - for WordPress URL
### 1.2 Data Storage Locations
| Location | Field | Purpose | Used By |
|----------|-------|---------|---------|
| **Django Site model** | `wp_api_key` | API key (source of truth) | Publishing, test-connection |
| **Django Site model** | `domain` | WordPress URL | Publishing |
| **WordPress wp_options** | `igny8_api_key` | Stored copy of API key | Plugin auth check |
| **WordPress wp_options** | `igny8_site_id` | Site ID from key | Plugin display |
| **WordPress wp_options** | `igny8_integration_id` | Integration ID | Plugin (currently unused) |
| **WordPress wp_options** | `igny8_last_structure_sync` | Last sync timestamp | Connection status UI |
### 1.3 SiteIntegration Model Status
**Table exists:** `igny8_site_integrations`
**Records:** 0 (empty)
**Used by:**
- `publishing_scheduler.py` - scheduled publishing (requires record)
- `wordpress_publishing.py` - Celery task path (requires record)
- `sync_metadata_service.py` - metadata sync (requires record)
- `ContentViewSet.publish` action (requires record)
**NOT used by:**
- `PublisherService` (main UI publishing path) - uses Site directly
- `WordPressAdapter` - uses Site.wp_api_key directly
---
## PART 2: SECURITY ISSUES FOUND
### 2.1 Plugin Public Endpoints (SECURITY RISK)
| Endpoint | Current Permission | Risk | Data Exposed |
|----------|-------------------|------|--------------|
| `/igny8/v1/status` | `__return_true` (PUBLIC) | Medium | Plugin installed, has_api_key, plugin version |
| `/igny8/v1/site-metadata/` | `__return_true` (PUBLIC*) | Low | Post types, taxonomies (auth checked inside) |
*Note: `/site-metadata/` checks permission inside callback but still registers as public
### 2.2 Backend test-connection Flow
The `test_connection_collection` endpoint:
1. Uses `AllowAny` permission class
2. BUT validates authentication inside the handler
3. Calls public `/status` endpoint to check if plugin installed
4. Then calls authenticated `/verify-key` endpoint
**Issue:** Relies on public `/status` endpoint to detect plugin presence.
---
## PART 3: RECOMMENDED CHANGES
### 3.1 Plugin Changes (REQUIRED)
#### 3.1.1 Make `/status` Endpoint Authenticated
**File:** `plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-rest-api.php`
**Current (lines 83-87):**
```php
register_rest_route('igny8/v1', '/status', array(
'methods' => 'GET',
'callback' => array($this, 'get_status'),
'permission_callback' => '__return_true', // Public endpoint for health checks
));
```
**Change to:**
```php
register_rest_route('igny8/v1', '/status', array(
'methods' => 'GET',
'callback' => array($this, 'get_status'),
'permission_callback' => array($this, 'check_api_key_auth'),
));
```
#### 3.1.2 Add Lightweight Auth Check Method
Add new method to allow API key OR no key if not configured yet:
```php
/**
* Check API key authentication for status-type endpoints
* Returns true if: no API key stored yet, OR valid API key provided
*/
public function check_api_key_auth($request) {
$stored_api_key = function_exists('igny8_get_secure_option')
? igny8_get_secure_option('igny8_api_key')
: get_option('igny8_api_key');
// If no API key configured yet, allow access (plugin not connected)
if (empty($stored_api_key)) {
return true;
}
// If API key is configured, require valid key in header
$header_api_key = $request->get_header('x-igny8-api-key');
if ($header_api_key && hash_equals($stored_api_key, $header_api_key)) {
return true;
}
return new WP_Error('rest_forbidden', 'Invalid API key', array('status' => 401));
}
```
#### 3.1.3 Fix `/site-metadata/` Route Registration
**Current (lines 74-79):**
```php
register_rest_route('igny8/v1', '/site-metadata/', array(
'methods' => 'GET',
'callback' => array($this, 'get_site_metadata'),
'permission_callback' => '__return_true',
));
```
**Change to:**
```php
register_rest_route('igny8/v1', '/site-metadata/', array(
'methods' => 'GET',
'callback' => array($this, 'get_site_metadata'),
'permission_callback' => array($this, 'check_permission'),
));
```
Then remove the internal permission check from `get_site_metadata()` method.
### 3.2 Backend Changes (REQUIRED)
#### 3.2.1 Update test-connection to Not Rely on Public Endpoint
**File:** `backend/igny8_core/modules/integration/views.py`
**Current approach:**
1. Call `/status` (public) to check plugin installed
2. Call `/verify-key` (authenticated) to verify key
**New approach:**
1. Call `/verify-key` directly
2. If 200 → plugin installed AND key valid
3. If 404 → plugin not installed (route doesn't exist)
4. If 401/403 → plugin installed but key mismatch
**Change Check 2 section (approximately lines 218-237):**
```python
# Check 2 & 3 Combined: Plugin installed + API key verification
# Use /verify-key endpoint - if it succeeds, both are confirmed
try:
verify_response = http_requests.get(
f"{site_url.rstrip('/')}/wp-json/igny8/v1/verify-key",
headers={
'X-IGNY8-API-KEY': stored_api_key,
'Content-Type': 'application/json'
},
timeout=10
)
if verify_response.status_code == 200:
health_checks['plugin_installed'] = True
health_checks['plugin_has_api_key'] = True
health_checks['api_key_verified'] = True
elif verify_response.status_code == 404:
# Route not found = plugin not installed
issues.append("IGNY8 plugin not installed on WordPress site")
elif verify_response.status_code in [401, 403]:
# Auth failed = plugin installed but key doesn't match
health_checks['plugin_installed'] = True
issues.append("API key mismatch - copy the API key from IGNY8 to WordPress plugin settings")
else:
issues.append(f"Unexpected response from plugin: HTTP {verify_response.status_code}")
except http_requests.exceptions.ConnectionError:
issues.append("Cannot connect to WordPress site")
except Exception as e:
issues.append(f"Connection error: {str(e)}")
```
### 3.3 SiteIntegration Decision (OPTIONAL)
The SiteIntegration model is used by:
- Scheduled publishing task
- ContentViewSet.publish action
- Metadata sync service
**Options:**
**Option A: Remove SiteIntegration entirely**
- Modify scheduled publishing to use Site directly
- Modify sync service to use Site directly
- Remove model and migrations
- **Effort:** High, risk of breaking things
**Option B: Keep but don't require it**
- Current main publishing path works without it
- Scheduled publishing would need records created
- **Effort:** Low, minimal changes
**Recommendation:** Option B - Keep as-is. The main UI publishing flow works. If scheduled publishing is needed, run:
```bash
docker exec igny8_backend python manage.py sync_wordpress_api_keys
```
---
## PART 4: IMPLEMENTATION ORDER
### Phase 1: Plugin Security (30 min)
1. Add `check_api_key_auth()` method to plugin
2. Change `/status` endpoint to use `check_api_key_auth`
3. Change `/site-metadata/` to use `check_permission`
4. Remove internal permission check from `get_site_metadata()`
5. Build and deploy plugin
### Phase 2: Backend Update (15 min)
1. Update `test_connection_collection` to use only `/verify-key`
2. Remove `/status` endpoint call
3. Test connection flow
### Phase 3: Testing (15 min)
1. Test plugin uninstalled scenario (404 from /verify-key)
2. Test plugin installed, no key (401 from /verify-key)
3. Test plugin installed, wrong key (401 from /verify-key)
4. Test plugin installed, correct key (200 from /verify-key)
5. Test publishing still works
---
## PART 5: FILES TO MODIFY
### Plugin Files
| File | Changes |
|------|---------|
| `includes/class-igny8-rest-api.php` | Add `check_api_key_auth()`, update route permissions |
### Backend Files
| File | Changes |
|------|---------|
| `modules/integration/views.py` | Update `test_connection_collection` logic |
---
## PART 6: WHAT STAYS THE SAME
**Publishing flow** - No changes needed
**Site.wp_api_key** - Single source of truth
**Site.domain** - WordPress URL source
**WordPressAdapter** - Works correctly
**Plugin /publish endpoint** - Already authenticated
**Plugin /verify-key endpoint** - Already authenticated
**SiteIntegration model** - Keep for scheduled publishing (optional use)
---
## Document History
| Date | Author | Changes |
|------|--------|---------|
| 2026-01-13 | AI | Created after proper code audit |