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

9.3 KiB

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.


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):

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:

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:

/**
 * 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):

register_rest_route('igny8/v1', '/site-metadata/', array(
    'methods' => 'GET',
    'callback' => array($this, 'get_site_metadata'),
    'permission_callback' => '__return_true',
));

Change to:

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):

# 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:

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