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_KEYenvironment variable ConnectorConfigauto-encrypts values markedis_secret=Trueon saveConnection.credentialsalways 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¶
- Create
apps/connectors/connectors/my_connector.py - Define an Actions class with
@action-decorated methods - Define a
BaseConnectorsubclass withslug,kind,display_name,actions_class - Implement the required auth methods
- Create a data migration to seed the
Connectorcatalog entry - Run
makemigrationsandmigrate
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