Skip to content

Connector System

The connector system provides a plugin architecture for integrating external services (Canvas LMS, Google Drive, OneDrive, PostgreSQL, ServiceNow, etc.) into CampusCore. It handles OAuth2 flows, encrypted credential storage, and a decorator-based action system that makes connectors self-describing.

Directory Structure

apps/connectors/
├── connectors/              # One subpackage per connector integration
│   ├── base.py              # BaseConnector abstract class
│   ├── registry.py          # Auto-discovery registry
│   ├── canvas/              # Canvas LMS connector
│   │   ├── __init__.py      #   re-exports CanvasConnector for discovery
│   │   ├── connector.py     #   CanvasConnector (OAuth flow, get_client)
│   │   └── actions.py       #   CanvasActions (@action methods)
│   ├── google_drive/        # Google Drive connector
│   │   ├── __init__.py
│   │   ├── connector.py
│   │   ├── actions.py
│   │   └── helpers.py       #   MIME mapping + drive-file normalizer
│   ├── onedrive/            # OneDrive connector (same shape as google_drive)
│   ├── postgres/            # PostgreSQL connector
│   │   ├── __init__.py
│   │   ├── connector.py
│   │   └── actions.py
│   ├── s3_bucket/           # Amazon S3 Bucket connector
│   │   ├── __init__.py
│   │   ├── connector.py
│   │   ├── actions.py
│   │   └── helpers.py       #   boto3 client builder, MIME + key utils
│   ├── servicenow/          # ServiceNow Table API connector
│   │   ├── __init__.py
│   │   ├── connector.py     #   OAuth flow, get_client closure
│   │   ├── actions.py       #   10 @action methods + validation constants
│   │   ├── models.py        #   Pydantic output models (IncidentSummary, etc.)
│   │   ├── helpers.py       #   reshape fns, URL builders, query builder
│   │   └── IMPLEMENTATION_PLAN.md
│   └── banner/              # Planned (doc only — no code yet)
│       └── IMPLEMENTATION_PLAN.md
├── actions/                 # Decorator-based action system
│   ├── decorator.py         # @action() decorator and ActionMeta
│   └── introspect.py        # Schema introspection (get_action_schemas, has_tag)
├── services/                # Shared services
│   ├── secrets.py           # Fernet encryption/decryption
│   ├── oauth.py             # OAuth2 session builder (authlib)
│   ├── http.py              # httpx client with tenacity retry
│   ├── token_updater.py     # Reusable OAuth2 token refresh callback
│   ├── import_service.py    # Cloud file import pipeline
│   ├── audit.py             # Action audit logging
│   └── rate_limit.py        # Cache-based rate limiting
├── models.py                # Connector, ConnectorConfig, Connection, ConnectorAuditLog
├── views.py                 # REST API endpoints (OAuth flows, actions, imports)
├── urls.py                  # URL routing
├── admin.py                 # Django admin configuration
└── tests/                   # Test suite

Core Concepts

Three-Model Pattern

Model Purpose
Connector Catalog entry (slug, name, kind, icon, is_enabled). One per integration type.
ConnectorConfig Admin-managed key-value config (CLIENT_ID, CLIENT_SECRET, etc.). Secret values are Fernet-encrypted on save.
Connection A user's authenticated link to a connector. Stores encrypted OAuth tokens or manual credentials. Tracks status and last_used_at.

BaseConnector

Every connector extends BaseConnector and must define:

class MyConnector(BaseConnector):
    slug = "my-service"          # Unique identifier, matches Connector.slug in DB
    kind = "oauth2"              # "oauth2" or "manual"
    display_name = "My Service"
    icon_url = "https://..."     # Optional
    actions_class = MyActions    # Class with @action-decorated methods

Required methods for subclasses: - connection_schema() - JSON Schema for manual credential forms (return None for OAuth2) - start_connect(request) - Begin OAuth2 flow, return {"redirect": url} - handle_callback(request) - Handle OAuth2 callback, return credentials dict - test_and_normalize_manual(form_data) - Validate manual credentials - get_client(credentials, token_updater) - Build authenticated API client

Authorization scoping

Connectors fall into two authorization models, and the model determines how much enforcement work CampusCore has to do itself. See connectors-philosophy.md §13 for the full discussion; this table is the quick reference.

Connector Model How scoping is enforced
Canvas upstream-scoped Per-user OAuth2 token stored on Connection.credentials; Canvas enforces "Alice can only see Alice's courses"
Google Drive upstream-scoped Per-user OAuth2 token; Google enforces delegated scope
OneDrive upstream-scoped Per-user OAuth2 token via Microsoft Graph delegated permissions
PostgreSQL locally-scoped (user-supplied) Each user enters their own DB credentials; upstream DB grants enforce scoping
S3 Bucket locally-scoped (user-supplied) Each user enters their own AWS credentials or IAM role; AWS enforces scoping
ServiceNow upstream-scoped Per-user OAuth2 token via the ServiceNow OAuth Application Registry; native ACLs (itil, knowledge, table-level rules) enforce read and write scoping. See the ServiceNow setup guide
Banner (Ethos) — planned locally-scoped (institution-wide) Institution-level ETHOS_API_KEY shared across users; CampusCore must enforce scoping via a BannerAuthorizationMiddleware before the action runs

Upstream-scoped connectors require no custom authorization logic — the vendor does the enforcement. Locally-scoped connectors split into two sub-flavors: user-supplied (each user brings their own credentials, so upstream scoping still works but at the connection level instead of the OAuth level) and institution-wide (one service-account credential is shared across all users, so CampusCore must enforce identity mapping + scoping middleware itself). The institution-wide flavor is the highest-responsibility: see the Banner implementation plan at banner_connector_implementation_plan.md for the required pattern.

Before adding a new connector, always decide which model it uses and document the choice in the connector's class docstring. The connector assistant skill will prompt for this explicitly.

Note on ServiceNow

Connectors philosophy §2 names ServiceNow as the one sanctioned exception for reaching for a vendor MCP server instead of hand-rolling. The v1 connector ships as kind="oauth2" Table API anyway — a deliberate, documented deviation to avoid greenfield platform work on kind="mcp" infrastructure. The full rationale lives in the connector's class docstring and in servicenow_connector_implementation_plan.md. Revisit once kind="mcp" transport lands.

Auto-Discovery Registry

Connectors are automatically discovered on import. Any .py file in connectors/connectors/ that defines a BaseConnector subclass with a non-empty slug is registered. No manual imports needed.

from apps.connectors.connectors.registry import get_connector, list_connectors

adapter = get_connector("google-drive")   # Get a specific connector
all_connectors = list_connectors()        # Get all registered connectors

Action System

@action Decorator

Actions are defined as decorated methods on an Actions class. The decorator captures tags for capability discovery. Type hints and docstrings provide schema information.

from apps.connectors.actions import action

class MyActions:
    def __init__(self, client):
        self.client = client

    @action("file_browse")
    def list_files(self, folder_id: str | None = None) -> dict:
        """List files and folders."""
        ...

    @action("file_download")
    def download_file(self, file_id: str) -> dict:
        """Download a file by ID."""
        ...

Tags

Tags are used for capability discovery without isinstance checks:

Tag Meaning
file_browse Connector can list files/folders (used by cloud import UI)
file_download Connector can download files (used by import pipeline)
read General read operations
from apps.connectors.actions.introspect import has_tag, get_action_schemas

# Check if a connector supports file browsing
if has_tag(adapter.actions_class, "file_browse"):
    ...

# Get all action schemas for a connector
schemas = get_action_schemas(adapter.actions_class)

Executing Actions

# Via BaseConnector (preferred)
result = adapter.execute_action("list_files", credentials, {"folder_id": "root"}, token_updater)

# Or via the legacy run_action (delegates to execute_action)
result = adapter.run_action("list_files", credentials, {"folder_id": "root"}, token_updater)

Cloud Drive Import Pipeline

When a user imports files from Google Drive or OneDrive into a knowledge folder:

User selects files in browser UI
    → POST to cloud_import_submit (HTMX endpoint)
    → ConnectorImportService.import_files()
        → adapter.execute_action("download_file", ...)  # Downloads file bytes
        → Upload bytes to S3 (same key pattern as direct uploads)
        → Create SourceFile record with status='uploaded'
        → Send SQS message via QueueService
        → Document processing pipeline handles the rest

The import service reuses the existing document processing pipeline — imported files are indistinguishable from directly uploaded files after the initial download.

HTMX Endpoints

Endpoint Purpose
GET /api/htmx/knowledge-folders/<folder_id>/cloud-import/ List all cloud drive connectors with Connect/Browse buttons
GET /api/htmx/knowledge-folders/<folder_id>/cloud-import/<connection_id>/browse/ Browse files in a connected drive
POST /api/htmx/knowledge-folders/<folder_id>/cloud-import/<connection_id>/import/ Import selected files

Rate Limits

Action Limit
Cloud file browsing 30 requests/minute per user
Cloud file import 10 imports/hour per user

OAuth2 Flow

User clicks "Connect" on a cloud drive
    → GET /api/connectors/connect/<slug>/start
    → adapter.start_connect(request) builds authorization URL
    → User redirected to provider (Google, Microsoft)
    → User authorizes
    → Provider redirects to /api/connectors/<slug>/callback
    → adapter.handle_callback(request) exchanges code for tokens
    → Tokens encrypted and stored in Connection model
    → User redirected to /settings with success message

OAuth2 tokens are automatically refreshed on use via the token_updater callback (built by make_token_updater()).

Credential Security

  • All credentials encrypted at rest using Fernet symmetric encryption
  • Encryption key stored in FERNET_KEY environment variable
  • ConnectorConfig auto-encrypts values marked is_secret=True on save
  • Connection.credentials always stored encrypted
  • Admin UI masks secret values via mask_sensitive_value()

Agent Integration

The ConnectorActionTool (in main_app/services/agent/tools/connector_tools.py) makes all connector actions available to the ReAct agent. It dynamically discovers the current user's connections and their available actions, presenting them as tool descriptions the agent can invoke.

Adding a New Connector

  1. Create apps/connectors/connectors/my_connector.py
  2. Define an Actions class with @action-decorated methods
  3. Define a BaseConnector subclass with slug, kind, display_name, actions_class
  4. Implement the required auth methods
  5. Create a data migration to seed the Connector catalog entry
  6. Run makemigrations and migrate

The connector is auto-discovered by the registry — no imports to update.

Audit Logging

Every action execution is logged to ConnectorAuditLog with: - Which connection was used - Action name - Success/error status - Request details (params, error info) - Timestamp