Files
steward/internal/api/types.go
T
abrown38andClaude Opus 5.5 3ad2f981d4 Allow and deny lists per subnet; every option of a DHCP message in words
- A subnet is a deny-list subnet (the default: everyone not denied) or
  an allow-list subnet (only clients allowed or with a reservation);
  the deny list counts in both. Entries are MAC addresses, prefixes
  (aa:bb:cc:*) or client IDs. A refused client gets no answer (DENIED
  in the activity log, with an Allow button) or a NAK on renewal.
  steward access, the subnet's Access tab. Schema migration 7.
- The activity log keeps each message's header fields and options,
  decoded into words (dhcp4.Describe): the parameter request list by
  name, client IDs, lease times. ▸ on an Activity row, steward
  activity show N / --details, GET /events/{id}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:27:51 +01:00

331 lines
11 KiB
Go

// Package api holds the JSON shapes of Steward's REST API, shared by the
// server and the Go client (and so the CLI).
package api
import (
"encoding/json"
"time"
"steward/internal/dhcp4"
"steward/internal/model"
"steward/internal/validate"
"steward/internal/version"
)
// Prefix starts every API path.
const Prefix = "/api/v1"
// ErrorBody is every error response.
type ErrorBody struct {
Error ErrorInfo `json:"error"`
}
// ErrorInfo describes an error.
type ErrorInfo struct {
Code string `json:"code"`
Message string `json:"message"`
Problems []validate.Problem `json:"problems,omitempty"`
// Result is the preview of a refused change.
Result json.RawMessage `json:"result,omitempty"`
}
// Whoami describes the caller.
type Whoami struct {
User *model.User `json:"user"`
Admin bool `json:"admin"`
Method string `json:"method"`
Permissions []string `json:"permissions"`
// TokenID is the token the caller authenticated with (0: none).
TokenID int64 `json:"token_id,omitempty"`
CSRFToken string `json:"csrf_token,omitempty"`
Build version.Build `json:"build"`
}
// Login is the login request.
type Login struct {
Username string `json:"username"`
Password string `json:"password"`
}
// PasswordChange changes the caller's password.
type PasswordChange struct {
Current string `json:"current"`
Password string `json:"password"`
}
// OptionInput sets an option: by name ("routers") or number ("43"), with
// a type for numbers the catalog doesn't know (default hex).
type OptionInput struct {
Option string `json:"option"`
Value string `json:"value"`
Type string `json:"type,omitempty"`
}
// SettingsInput changes the settings (unset fields stay).
type SettingsInput struct {
LeaseTime *int `json:"lease_time,omitempty"`
Options *[]OptionInput `json:"options,omitempty"`
PingCheck *bool `json:"ping_check,omitempty"`
PingTimeout *int `json:"ping_timeout_ms,omitempty"`
DeclineHold *int `json:"decline_hold,omitempty"`
NotAuthoritative *bool `json:"not_authoritative,omitempty"`
// AddressOrder is sequential, reverse or random.
AddressOrder *string `json:"address_order,omitempty"`
}
// SubnetInput creates or changes a subnet (unset fields stay). Pools are
// "FIRST-LAST" or prefixes, with ":ORDER" for a pool's own address order.
type SubnetInput struct {
Name *string `json:"name,omitempty"`
Interface *string `json:"interface,omitempty"`
Prefix *string `json:"prefix,omitempty"`
Pools *[]string `json:"pools,omitempty"`
// PoolOrder is listed or random; AddressOrder sequential, reverse,
// random or "" (the settings').
PoolOrder *string `json:"pool_order,omitempty"`
AddressOrder *string `json:"address_order,omitempty"`
// Access is deny-list or allow-list; Allow and Deny replace the
// lists (entries: a MAC address, aa:bb:cc:* or "id HEX").
Access *string `json:"access,omitempty"`
Allow *[]model.ClientMatch `json:"allow,omitempty"`
Deny *[]model.ClientMatch `json:"deny,omitempty"`
LeaseTime *int `json:"lease_time,omitempty"`
Options *[]OptionInput `json:"options,omitempty"`
Disabled *bool `json:"disabled,omitempty"`
Comment *string `json:"comment,omitempty"`
}
// ReservationInput creates or changes a reservation (unset fields stay).
type ReservationInput struct {
MAC *string `json:"mac,omitempty"`
ClientID *string `json:"client_id,omitempty"`
IP *string `json:"ip,omitempty"`
Hostname *string `json:"hostname,omitempty"`
Options *[]OptionInput `json:"options,omitempty"`
// LeaseTime in seconds; 0 for the subnet's.
LeaseTime *int `json:"lease_time,omitempty"`
Disabled *bool `json:"disabled,omitempty"`
Comment *string `json:"comment,omitempty"`
}
// Subnet is a subnet with its usage.
type Subnet struct {
*model.Subnet
Usage Usage `json:"usage"`
}
// Usage counts a subnet's addresses.
type Usage struct {
// PoolSize is the number of addresses in the pools; Leased of them
// are held by clients, Declined held back.
PoolSize int `json:"pool_size"`
Leased int `json:"leased"`
Declined int `json:"declined"`
// Reservations in the subnet, and how many of them have a lease now.
Reservations int `json:"reservations"`
ReservationsLeased int `json:"reservations_leased"`
}
// Reservation is a reservation with where it is.
type Reservation struct {
*model.Reservation
SubnetID int64 `json:"subnet_id"`
SubnetName string `json:"subnet"`
// Lease is the reservation's current lease, if any.
Lease *Lease `json:"lease,omitempty"`
}
// Lease is a lease with its state now.
type Lease struct {
*model.Lease
SubnetName string `json:"subnet"`
// Reservation is the ID of the reservation for the address, if any.
Reservation int64 `json:"reservation,omitempty"`
// ReservedHostname is the reservation's host name.
ReservedHostname string `json:"reserved_hostname,omitempty"`
}
// MutationResponse answers a configuration change.
type MutationResponse struct {
Applied bool `json:"applied"`
DryRun bool `json:"dry_run,omitempty"`
HistoryID int64 `json:"history_id,omitempty"`
Changes []model.Change `json:"changes"`
Warnings []validate.Problem `json:"warnings,omitempty"`
// Object is the created or changed object.
Object json.RawMessage `json:"object,omitempty"`
}
// HostInterface is one of the server's network interfaces.
type HostInterface struct {
Name string `json:"name"`
Addresses []string `json:"addresses"`
// Subnets served on it.
Subnets []int64 `json:"subnets"`
}
// InterfaceStatus says whether DHCP is served on an interface.
type InterfaceStatus struct {
Name string `json:"name"`
Listening bool `json:"listening"`
Error string `json:"error,omitempty"`
Since time.Time `json:"since"`
Addresses []string `json:"addresses"`
Subnets []int64 `json:"subnets"`
}
// Status is the dashboard's overview.
type Status struct {
Build version.Build `json:"build"`
Started time.Time `json:"started"`
Interfaces []InterfaceStatus `json:"interfaces"`
Subnets []Subnet `json:"subnets"`
Problems []validate.Problem `json:"problems"`
// Leases counts leases by state.
Leases map[string]int `json:"leases"`
// Messages counts DHCP messages since the start, by type.
Messages map[string]uint64 `json:"messages"`
// PingProblem says why addresses can't be pinged before an offer.
PingProblem string `json:"ping_problem,omitempty"`
Database string `json:"database"`
}
// TokenInput creates a token.
type TokenInput struct {
Name string `json:"name"`
// Grants ("leases:read") or a role restrict the token; none: all of
// the user's permissions.
Grants []string `json:"grants,omitempty"`
Role string `json:"role,omitempty"`
ExpiresIn string `json:"expires_in,omitempty"` // 90d, 720h
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
// TokenRotation gives a token a new secret. Grace keeps the old secret
// working for a while (up to 7d). The token gets its lifetime again,
// counted from now, unless ExpiresIn or NoExpiry says otherwise.
type TokenRotation struct {
Grace string `json:"grace,omitempty"`
ExpiresIn string `json:"expires_in,omitempty"`
NoExpiry bool `json:"no_expiry,omitempty"`
}
// Token is a token; Secret is only set when it is created.
type Token struct {
*model.Token
GrantStrings []string `json:"grant_strings"`
Secret string `json:"secret,omitempty"`
}
// UserInput creates or changes a user.
type UserInput struct {
Username *string `json:"username,omitempty"`
Password *string `json:"password,omitempty"`
Admin *bool `json:"admin,omitempty"`
Grants *[]string `json:"grants,omitempty"`
Role *string `json:"role,omitempty"`
Disabled *bool `json:"disabled,omitempty"`
Comment *string `json:"comment,omitempty"`
}
// User is a user with grants written out.
type User struct {
*model.User
GrantStrings []string `json:"grant_strings"`
}
// OptionDef is an option of the catalog.
type OptionDef struct {
Code int `json:"code"`
Name string `json:"name"`
Type string `json:"type"`
Help string `json:"help"`
Common bool `json:"common,omitempty"`
}
// Catalog lists the options Steward knows, the value types and the codes
// that can't be set.
type Catalog struct {
Options []OptionDef `json:"options"`
Types []string `json:"types"`
Reserved map[int]string `json:"reserved"`
Roles map[string][]string `json:"roles"`
}
// Event is a DHCP message in or out.
type Event struct {
Time time.Time `json:"time"`
Interface string `json:"interface"`
Type string `json:"type"`
MAC string `json:"mac"`
IP string `json:"ip,omitempty"`
Hostname string `json:"hostname,omitempty"`
Detail string `json:"detail,omitempty"`
// ID numbers the events since stewardd started.
ID uint64 `json:"id"`
// Direction is "in" for a client's message, "out" for Steward's
// answer, "" for an event of Steward's own (CONFLICT, NO ADDRESS).
Direction string `json:"direction,omitempty"`
ClientID string `json:"client_id,omitempty"`
// SubnetID is the subnet that refused a client (DENIED).
SubnetID int64 `json:"subnet_id,omitempty"`
// Fields and Options are the message, in words (GET /events/{id}, or
// ?details=1).
Fields []dhcp4.Field `json:"fields,omitempty"`
Options []dhcp4.Field `json:"options,omitempty"`
}
// ImportResult reports a Kea import.
type ImportResult struct {
MutationResponse
Leases int `json:"leases"`
Notes []string `json:"notes"`
Skipped []string `json:"skipped"`
}
// CertStep is one step of the preflight check.
type CertStep struct {
Name string `json:"name"`
Status string `json:"status"` // ok, fail, warn, skip
Detail string `json:"detail"`
// Queries are the DNS questions asked, as dig commands.
Queries []string `json:"queries,omitempty"`
}
// CertCheck is the result of a preflight check: can Steward publish its
// challenge through Jarl, and does the internet see it?
type CertCheck struct {
Time time.Time `json:"time"`
Domain string `json:"domain"`
OK bool `json:"ok"`
Steps []CertStep `json:"steps"`
}
// CertStatus describes the certificate, for the API and the dashboard.
type CertStatus struct {
Domain string `json:"domain"`
Directory string `json:"directory"`
Staging bool `json:"staging"`
State string `json:"state"` // none, ok, working, error
NotBefore *time.Time `json:"not_before,omitempty"`
NotAfter *time.Time `json:"not_after,omitempty"`
Issuer string `json:"issuer,omitempty"`
// IssuedBy is the directory the current certificate came from (it may
// differ from Directory until the next renewal).
IssuedBy string `json:"issued_by,omitempty"`
RenewAt *time.Time `json:"renew_at,omitempty"`
LastError string `json:"last_error,omitempty"`
LastAttempt *time.Time `json:"last_attempt,omitempty"`
NextAttempt *time.Time `json:"next_attempt,omitempty"`
Check *CertCheck `json:"check,omitempty"`
}
// CertificateResponse is GET /certificate.
type CertificateResponse struct {
// Configured is false when tls.acme is off.
Configured bool `json:"configured"`
// Source is acme, file or none.
Source string `json:"source"`
Status *CertStatus `json:"status,omitempty"`
}