- stewardd init asks for the first administrator's name on a terminal.
- Tokens can be rotated (steward token rotate, POST /tokens/{id}/rotate,
Rotate in the web UI), with a grace period for the old secret, as in
Jarl. Schema migration 2 adds the token columns.
- Read-only users see the settings and a subnet's settings locked,
instead of editable fields without a Save button.
- The New token dialog only offers roles within what the user may do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
258 lines
8.3 KiB
Go
258 lines
8.3 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/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"`
|
|
}
|
|
|
|
// SubnetInput creates or changes a subnet (unset fields stay). Pools are
|
|
// "FIRST-LAST" or prefixes.
|
|
type SubnetInput struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Interface *string `json:"interface,omitempty"`
|
|
Prefix *string `json:"prefix,omitempty"`
|
|
Pools *[]string `json:"pools,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"`
|
|
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"`
|
|
}
|
|
|
|
// ImportResult reports a Kea import.
|
|
type ImportResult struct {
|
|
MutationResponse
|
|
Leases int `json:"leases"`
|
|
Notes []string `json:"notes"`
|
|
Skipped []string `json:"skipped"`
|
|
}
|