contract

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package contract lets a plugin author verify a built plugin satisfies the OSCTF contract WITHOUT the monorepo — it dials the plugin exactly as the host does, wrapped so no wire type is exposed. Call it from a Go test (the template's plugin_test.go does): Build your plugin, then Verify* it. Designed alongside the SDK, not bolted on after, so it checks what matters (the handshake, the advertised type/ABI, and per-case behaviour) rather than what is convenient.

Index

Constants

This section is empty.

Variables

View Source
var ReservedIdentityClaims = []string{
	"admin", "is_admin", "isadmin",
	"role", "roles",
	"user_id", "userid", "uid",
	"scope", "scopes",
	"banned", "hidden",
}

ReservedIdentityClaims is exported so an author can screen their own claim map before returning it. It mirrors the keys the HOST refuses on the return path (the authoritative list is reservedClaimKeys in internal/auth/external.go). A plugin that emits one of these does not gain authority — the host rejects the whole login — so an author who ships them breaks every login for their users. Catching it here costs a test run; catching it in production costs an event. TestContractReservedClaimsMatchHost pins this against the host's list.

Functions

func Build

func Build(tb testing.TB, dir string) string

Build compiles the plugin whose main package is in dir to a temporary binary and returns its path, for passing to a Verify* function. It fails the test on a build error.

func VerifyAuth

func VerifyAuth(tb testing.TB, binaryPath string, cases AuthCases)

VerifyAuth launches the auth plugin at binaryPath and asserts the contract.

Common to both capabilities: the handshake succeeds; Info advertises the auth type with a non-empty name and ABI; and the advertised capability set is non-empty and contains only "password" and/or "redirect".

Capability-gated, and checked in BOTH directions — a capability the plugin advertises must work, and one it does not advertise must fail closed rather than half-answer:

  • password: every BadCredential is rejected; GoodCredential (if given) authenticates and yields a non-empty subject; and no returned identity carries a reserved claim.
  • redirect: Begin returns an authorize URL whose `state` parameter is EXACTLY the state passed in — the host verifies this too and refuses a login otherwise, so a plugin that invents its own state cannot log anyone in. Complete with no authorization code is rejected rather than returning a bare identity.

func VerifyChallengeType

func VerifyChallengeType(tb testing.TB, binaryPath string, cases ChallengeTypeCases)

VerifyChallengeType launches the challenge-type plugin at binaryPath and asserts the contract that matters for a type that decides correctness:

  • the handshake succeeds and Info advertises the challenge-type with a non-empty name/ABI;
  • ValidateConfig ACCEPTS ValidConfig and REJECTS each RejectedConfig with a per-field error;
  • CheckFlag is DETERMINISTIC (same input → same answer; a checker with hidden state is a bug);
  • CheckFlag accepts Correct, rejects Incorrect, and ERRORS on Undecidable (fail-closed, so no attempt is burned) — all run against the NORMALIZED config the plugin itself returned.

func VerifyNotification

func VerifyNotification(tb testing.TB, binaryPath string, cases []NotificationCase)

VerifyNotification launches the notification plugin at binaryPath and asserts the contract: the handshake succeeds, Info advertises the notification type with a non-empty name and ABI, Subscriptions is callable, and each case's event is delivered without a transport error. (It does not assert side effects — a notifier's real output is external; the contract is that the plugin accepts a subscribed event and does not fail the delivery RPC.)

func VerifyScoring

func VerifyScoring(tb testing.TB, binaryPath string, cases []ScoringCase)

VerifyScoring launches the scoring plugin at binaryPath and asserts the contract: the handshake succeeds, Info advertises the scoring type with a non-empty name and ABI, each case's Value matches, and the scorer is deterministic (a pure scorer returns the same value for the same input). Reports failures through tb, so call it from a Go test.

Types

type AuthCases

type AuthCases struct {
	// RedirectURI is the host callback URL handed to Begin. Required for a redirect plugin.
	RedirectURI string

	// BadCredentials MUST all be rejected. A password plugin that returns an identity for a
	// credential it could not verify authenticates anyone.
	BadCredentials []AuthCredential

	// GoodCredential, when set, must authenticate successfully.
	GoodCredential *AuthCredential
	// WantSubject, when set, is the subject GoodCredential must yield.
	WantSubject string
}

AuthCases configures VerifyAuth.

CONFIGURING THE PLUGIN UNDER TEST: an auth plugin almost always needs config (an issuer, a client id, a secret). The harness launches your binary as a child process, so it inherits the test process's environment — set the host's config variable before calling, e.g.

t.Setenv("OSCTF_PLUGIN_CONFIG", `{"issuer":"`+srv.URL+`","client_id":"test","client_secret":"s"}`)

A redirect plugin pointed at a test issuer (an httptest server serving the discovery document and JWKS) can then be exercised for real.

type AuthCredential

type AuthCredential struct {
	Name       string
	Identifier string
	Secret     string
}

AuthCredential is one identifier/secret pair for VerifyAuth to try against a password-capable plugin.

type ChallengeTypeCases

type ChallengeTypeCases struct {
	// ValidConfig is a config the type accepts. Its NORMALIZED form — what the host stores and later
	// hands back to CheckFlag — is what the flag cases below run against, so the checks exercise
	// production's stored value, not the raw input.
	ValidConfig map[string]string

	// RejectedConfigs are configs the type MUST reject at author time, each with at least one
	// per-field error. At least one is required: a ValidateConfig that returns OK unconditionally (a
	// common author mistake) must fail this verifier.
	RejectedConfigs []map[string]string

	// Correct are submissions CheckFlag must accept — (true, no error). At least one required.
	Correct []string
	// Incorrect are submissions CheckFlag must reject — (false, no error). At least one required.
	Incorrect []string
	// Undecidable are submissions CheckFlag must fail on with an ERROR, not a false. The host fails
	// CLOSED on an error and consumes NO attempt; returning false instead silently burns a player's
	// attempt — the single most consequential thing a checker author can get wrong. Optional: a
	// checker that can always decide (a regex either matches or not) has none, but if yours can hit
	// an internal failure (a missing key, an external call), prove it errors rather than guessing.
	Undecidable []string
}

ChallengeTypeCases specifies the author-supplied inputs VerifyChallengeType checks. A challenge-type plugin DECIDES CORRECTNESS, so the contract is behavioural — not just "the RPCs answer": provide config the type accepts and rejects, and flags it must accept, reject, and (if it can hit an internal failure) refuse to decide.

type NotificationCase

type NotificationCase struct {
	Name  string
	Event sdk.Event
}

NotificationCase is one event delivered to the plugin under test by VerifyNotification.

type ScoringCase

type ScoringCase struct {
	Name string
	In   sdk.Score
	Want int
}

ScoringCase is one input and its expected value for VerifyScoring.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL