conductorone@engineering ~
$ cd /engineering && cat ./one-mock-server-twelve-protocols.md
Formal Methods

> One Mock Server, Twelve Protocols

/images/author-rch.png
6 min read
share:
width:

One Mock Server, Twelve Protocols

Adding a new connector to the verification framework requires zero Go code. Every new connector is a YAML file.

This is part of a series on formally verifying identity connectors. The verification framework needs to control a connector’s inputs precisely – which users exist, which groups they belong to, what roles are assigned. That means mocking the upstream API. Two hundred connectors speak more than a dozen different protocols. The mock server handles all of them behind one interface.


The Problem

A connector talks to an external system – Okta’s REST API, PostgreSQL’s wire protocol, Active Directory over LDAP, AWS via SDK calls. To verify a connector, the framework runs it against controlled inputs and checks the output against a Datalog specification. “Controlled inputs” means serving convincing responses over whatever protocol the connector expects.

That’s twelve different wire protocols: HTTP/REST, GraphQL, MySQL, PostgreSQL, Oracle TNS, LDAP, Redis, DynamoDB, gRPC, and cloud SDK variants for AWS, GCP, and Azure. Each with different serialization, different connection handling, different authentication flows.

Building separate test fixtures for each connector would mean hundreds of mocks, each encoding the same structural patterns in different ways. Instead: one interface, twelve protocol adapters, YAML per connector.


The Interface

type Mock interface {
    Configure(input any) error
    Start() (endpoint string, err error)
    Stop() error
    Schema() InputSchema
    IncrementalUpdate(delta Delta) error
}

Configure takes a set of entities – users, groups, roles, memberships – and loads them into the mock’s state. The input format is protocol-agnostic: the same structure describes “three users with two roles each” whether the connector will query it over REST or LDAP.

Start spins up a listener on the appropriate protocol. A REST mock binds an HTTP port and registers route handlers. A PostgreSQL mock opens a socket and speaks the Postgres wire protocol. An LDAP mock runs a directory server. The returned endpoint string is what the connector uses as its target.

Stop tears down the listener.

Schema returns the input space definition: which dimensions exist (users, groups, roles, permissions), what their ranges are, and which combinations are valid. This is what the Gray code traversal uses to explore the configuration space. New connectors get automatic input space discovery from their YAML config alone.

IncrementalUpdate is where it gets interesting. Instead of reconfiguring the entire mock for each point in the input space, a delta describes a single change:

type Delta struct {
    Dimension string
    OldValue  any
    NewValue  any
    Operation DeltaOp
}

const (
    DeltaSet DeltaOp = iota
    DeltaAdd
    DeltaRemove
    DeltaModify
)

Gray code changes one dimension per step. IncrementalUpdate changes one thing in the mock. The mock never restarts between probes – it mutates in place, and the connector syncs against the updated state. This is what makes trillion-point traversals practical: each step is a delta, not a rebuild.


Protocol Adapters

Each adapter translates between protocol-agnostic entity state and a specific wire format:

type ProtocolAdapter interface {
    SetFacts(facts []Fact)
    Start() (string, error)
    Stop() error
    IncrementalUpdate(delta Delta) error
}

HTTP/REST is the most common – about 97% of connectors use it. The adapter maps entity types to URL patterns and serves JSON responses. Pagination strategies (cursor, page-based, link headers), rate limiting headers, and authentication endpoints are all configurable per connector.

The HTTP adapter handles auth realistically. An embedded OAuth mock implements client_credentials and refresh_token grants, serves tokens with configurable TTL, and validates bearer tokens on subsequent requests:

type OAuthMock struct {
    clientID      string
    clientSecret  string
    tokenTTL      time.Duration
    tokens        map[string]*TokenInfo
    refreshTokens map[string]*RefreshTokenInfo
}

Connectors that authenticate via OAuth go through the same flow against the mock that they would against the real API.

GraphQL serves a schema and resolves queries against the mock’s entity state. GitHub’s API is GraphQL-first, so the GitHub connector’s mock responds to the same nested queries the real API handles.

SQL protocols (MySQL, PostgreSQL, Oracle TNS) implement the wire protocol at the connection level. The mock accepts connections, authenticates, and responds to queries against in-memory tables populated from the config. The PostgreSQL adapter handles the startup message, authentication exchange, query parsing, and result serialization.

LDAP runs a directory server with entries constructed from the entity state. Active Directory and other directory connectors query it with standard LDAP search operations.

Cloud SDKs (AWS, GCP, Azure) are HTTP-based but require specific authentication flows and response formats. The AWS adapter handles SigV4 signing verification. The GCP and Azure adapters handle their respective auth patterns.

gRPC serves protocol buffer definitions and handles streaming RPCs. Redis and DynamoDB speak their native protocols for connectors that read ACLs or identity data from those systems.

Adding a new protocol: implement the ProtocolAdapter interface. Adding a new connector on an existing protocol: write a YAML config.


YAML Configuration

Each connector’s verification config specifies how to drive the mock:

name: GitHub
binary_path: ./baton-github
cli_template: "--token={{.APIToken}} --orgs={{.Orgs}}"
env_vars:
  BATON_API_TOKEN: "{{.APIToken}}"

mock:
  protocol: http
  auth:
    type: bearer

The cli_template is a Go template that handles each connector’s CLI arguments. Different connectors authenticate differently – API tokens, client credentials, username/password, DSN strings, kubeconfig paths – and the template system handles all of them without special-casing.

The mock’s entity state comes from the verification framework, not from the YAML config. The config says “this connector speaks HTTP with bearer auth.” The framework says “here are 5 users, 3 groups, and 8 memberships – serve them.” The separation means the same config works for any point in the input space.

The connector configs also bind axiom predicates to API data through predicate mappings:

predicate_mappings:
- axiom_predicate: team_member
  entitlement: member
  resource_type: team
  principal_type: user
  args:
    - "cel:principal.id"
    - "cel:resource.id"
    - "cel:principal.annotations.get('github/role', 'member')"

CEL expressions evaluate JSON fields from API responses and map them to Datalog predicate arguments. This is how the verification framework knows that team_member("alice", "engineering", "member") corresponds to Alice being listed in GitHub’s /orgs/acme/teams/engineering/members endpoint.


Sync Verification

The mock enables a second kind of verification beyond point-in-time correctness: sync behavior.

A connector isn’t just supposed to produce the right output for a static snapshot. It’s supposed to detect changes. The framework configures a baseline state, runs the connector, applies a delta via IncrementalUpdate – add a user, remove a group membership, change a role – runs the connector again, and verifies that the diff in output matches the diff in input. If the mock added a user and the connector’s sync didn’t pick it up, that’s a Missing fact.

This catches a specific class of bugs that point-in-time verification misses: pagination boundary errors during incremental sync, race conditions in change detection, and off-by-one errors in cursor handling.

Series

This is part of a series on formally verifying identity connectors:

  1. Six Shapes of Authorization
  2. Formally Verifying Two Hundred Identity Connectors
  3. One Mock Server, Twelve Protocols (this post)
  4. Every Branch Condition Compiles to a DFA
  5. Proving Equivalence with E-Graphs
  6. Fault Injection for Complete Branch Coverage