Files
DDI/server/internal/httpapi/routes.go
T
sir_melonchenandClaude Opus 5.5 2b04661792 feat: clusters, TSIG keys, zones and records in service and REST API
Implements the M5 endpoints of the contract (ADR 0023): clusters with
member validation, transfer keys and HA credentials, TSIG keys with
audited secret access and rotation, explicit zones and typed records,
the DHCP cluster of networks, the cluster of agents and the default
clusters and HA password file of the generator settings. Revisions are
stored per target with sealed secret files and sealed source secrets
(restored for rollbacks); bundles carry an agent's own target or the
default group. Active leases are adopted as dynamic addresses.

The derivation is called through a temporary adapter until derive
implements the M5 contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:47:37 +02:00

140 lines
8.5 KiB
Go

// SPDX-License-Identifier: LicenseRef-DDI-Proprietary
package httpapi
import (
"net/http"
"git.beugsiud.de/beug.k/DDI/server/internal/auth"
)
// Routes returns the route table. It is the single place that assigns
// permissions; TestEveryRouteIsProtected and the OpenAPI contract test check it.
func (a *API) Routes() []Route {
const (
get, post, put, patch, del = http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete
)
perm := func(method, path string, p auth.Permission, write bool, h handlerFunc) Route {
return Route{Method: method, Path: path, Access: permission, Perm: p, Write: write, Handler: h}
}
return []Route{
// System and authentication.
{Method: get, Path: "/api/v1/system/info", Access: public, Handler: a.systemInfo},
{Method: post, Path: "/api/v1/auth/login", Access: public, Handler: a.login},
{Method: get, Path: "/api/v1/auth/oidc/start", Access: public, Handler: a.oidcStart},
{Method: get, Path: "/api/v1/auth/oidc/callback", Access: public, Handler: a.oidcCallback},
{Method: get, Path: "/api/v1/auth/session", Access: session, MFAPending: true, PasswordChange: true, Handler: a.getSession},
{Method: post, Path: "/api/v1/auth/logout", Access: session, MFAPending: true, PasswordChange: true, Handler: a.logout},
{Method: post, Path: "/api/v1/auth/totp/verify", Access: session, MFAPending: true, Handler: a.totpVerify},
{Method: post, Path: "/api/v1/auth/password", Access: session, PasswordChange: true, Handler: a.changePassword},
{Method: post, Path: "/api/v1/auth/totp/setup", Access: session, Handler: a.totpSetup},
{Method: post, Path: "/api/v1/auth/totp/enable", Access: session, Handler: a.totpEnable},
{Method: post, Path: "/api/v1/auth/totp/disable", Access: session, Handler: a.totpDisable},
// Users.
perm(get, "/api/v1/users", auth.PermUsersAdmin, false, a.listUsers),
perm(post, "/api/v1/users", auth.PermUsersAdmin, true, a.createUser),
perm(get, "/api/v1/users/{id}", auth.PermUsersAdmin, false, a.getUser),
perm(put, "/api/v1/users/{id}", auth.PermUsersAdmin, true, a.updateUser),
perm(del, "/api/v1/users/{id}", auth.PermUsersAdmin, true, a.deleteUser),
perm(post, "/api/v1/users/{id}/reset-password", auth.PermUsersAdmin, true, a.resetPassword),
// Audit log.
perm(get, "/api/v1/audit", auth.PermAuditRead, false, a.listAudit),
perm(get, "/api/v1/audit/export", auth.PermAuditRead, false, a.exportAudit),
// License (never write-locked: it is the way out of read-only mode).
perm(get, "/api/v1/license", auth.PermLicenseRead, false, a.getLicense),
perm(put, "/api/v1/license", auth.PermLicenseAdmin, false, a.setLicense),
perm(del, "/api/v1/license", auth.PermLicenseAdmin, false, a.removeLicense),
perm(post, "/api/v1/license/file", auth.PermLicenseAdmin, false, a.importLicenseFile),
perm(post, "/api/v1/license/refresh", auth.PermLicenseAdmin, false, a.refreshLicense),
// Settings.
perm(get, "/api/v1/settings/oidc", auth.PermSettingsAdm, false, a.getOIDCSettings),
perm(put, "/api/v1/settings/oidc", auth.PermSettingsAdm, true, a.setOIDCSettings),
perm(get, "/api/v1/settings/generator", auth.PermSettingsAdm, false, a.getGeneratorSettings),
perm(put, "/api/v1/settings/generator", auth.PermSettingsAdm, true, a.setGeneratorSettings),
// Generated configuration (read-only preview until agents arrive).
perm(get, "/api/v1/config/preview", auth.PermIPAMRead, false, a.previewConfig),
perm(get, "/api/v1/config/pending", auth.PermIPAMRead, false, a.pendingChanges),
// Live updates (Server-Sent Events, ADR 0022).
perm(get, "/api/v1/events", auth.PermIPAMRead, false, a.streamEvents),
// Agents and enrollment (ADR 0021).
perm(get, "/api/v1/agents", auth.PermIPAMRead, false, a.listAgents),
perm(get, "/api/v1/agents/{id}", auth.PermIPAMRead, false, a.getAgent),
perm(patch, "/api/v1/agents/{id}", auth.PermSettingsAdm, true, a.updateAgent),
// Locking agents out is a security function and stays available in
// license read-only mode, like deleting enrollment tokens.
perm(del, "/api/v1/agents/{id}", auth.PermSettingsAdm, false, a.deleteAgent),
perm(post, "/api/v1/agents/{id}/revoke", auth.PermSettingsAdm, false, a.revokeAgent),
perm(get, "/api/v1/enrollment-tokens", auth.PermSettingsAdm, false, a.listEnrollmentTokens),
perm(post, "/api/v1/enrollment-tokens", auth.PermSettingsAdm, true, a.createEnrollmentToken),
perm(del, "/api/v1/enrollment-tokens/{id}", auth.PermSettingsAdm, false, a.deleteEnrollmentToken),
// Revisions, deployments and leases.
perm(get, "/api/v1/revisions", auth.PermIPAMRead, false, a.listRevisions),
perm(post, "/api/v1/revisions", auth.PermDeploy, true, a.createRevision),
perm(get, "/api/v1/revisions/{number}", auth.PermIPAMRead, false, a.getRevision),
perm(post, "/api/v1/revisions/{number}/deploy", auth.PermDeploy, true, a.deployRevision),
perm(post, "/api/v1/revisions/{number}/rollback", auth.PermDeploy, true, a.rollbackRevision),
perm(get, "/api/v1/revisions/{number}/diff", auth.PermIPAMRead, false, a.diffRevision),
perm(get, "/api/v1/deployments", auth.PermIPAMRead, false, a.listDeployments),
perm(get, "/api/v1/leases", auth.PermIPAMRead, false, a.listLeases),
// Networks.
perm(get, "/api/v1/networks", auth.PermIPAMRead, false, a.listNetworks),
perm(post, "/api/v1/networks", auth.PermIPAMWrite, true, a.createNetwork),
perm(get, "/api/v1/networks/{id}", auth.PermIPAMRead, false, a.getNetwork),
perm(put, "/api/v1/networks/{id}", auth.PermIPAMWrite, true, a.updateNetwork),
perm(del, "/api/v1/networks/{id}", auth.PermIPAMWrite, true, a.deleteNetwork),
perm(get, "/api/v1/networks/{id}/map", auth.PermIPAMRead, false, a.networkMap),
// Addresses.
perm(get, "/api/v1/addresses", auth.PermIPAMRead, false, a.listAddresses),
perm(post, "/api/v1/addresses", auth.PermIPAMWrite, true, a.createAddress),
perm(get, "/api/v1/addresses/export", auth.PermIPAMRead, false, a.exportAddresses),
perm(post, "/api/v1/addresses/import", auth.PermIPAMWrite, true, a.importAddresses),
perm(post, "/api/v1/addresses/preview", auth.PermIPAMRead, false, a.previewAddress),
perm(post, "/api/v1/addresses/bulk", auth.PermIPAMWrite, true, a.bulkAddresses),
perm(get, "/api/v1/addresses/{id}", auth.PermIPAMRead, false, a.getAddress),
perm(put, "/api/v1/addresses/{id}", auth.PermIPAMWrite, true, a.updateAddress),
perm(del, "/api/v1/addresses/{id}", auth.PermIPAMWrite, true, a.deleteAddress),
// Clusters and TSIG keys (ADR 0023): read with ipam.read (never
// secrets), changed by administrators. Reading a key's secret
// changes nothing and stays available in read-only mode.
perm(get, "/api/v1/clusters", auth.PermIPAMRead, false, a.listClusters),
perm(post, "/api/v1/clusters", auth.PermSettingsAdm, true, a.createCluster),
perm(get, "/api/v1/clusters/{id}", auth.PermIPAMRead, false, a.getCluster),
perm(put, "/api/v1/clusters/{id}", auth.PermSettingsAdm, true, a.updateCluster),
perm(del, "/api/v1/clusters/{id}", auth.PermSettingsAdm, true, a.deleteCluster),
perm(get, "/api/v1/tsig-keys", auth.PermIPAMRead, false, a.listTSIGKeys),
perm(post, "/api/v1/tsig-keys", auth.PermSettingsAdm, true, a.createTSIGKey),
perm(put, "/api/v1/tsig-keys/{id}", auth.PermSettingsAdm, true, a.updateTSIGKey),
perm(del, "/api/v1/tsig-keys/{id}", auth.PermSettingsAdm, true, a.deleteTSIGKey),
perm(get, "/api/v1/tsig-keys/{id}/secret", auth.PermSettingsAdm, false, a.getTSIGKeySecret),
perm(post, "/api/v1/tsig-keys/{id}/rotate", auth.PermSettingsAdm, true, a.rotateTSIGKey),
// Zones and records (ADR 0023).
perm(get, "/api/v1/zones", auth.PermIPAMRead, false, a.listZones),
perm(post, "/api/v1/zones", auth.PermIPAMWrite, true, a.createZone),
perm(get, "/api/v1/zones/derived", auth.PermIPAMRead, false, a.listDerivedZones),
perm(get, "/api/v1/zones/{id}", auth.PermIPAMRead, false, a.getZone),
perm(put, "/api/v1/zones/{id}", auth.PermIPAMWrite, true, a.updateZone),
perm(del, "/api/v1/zones/{id}", auth.PermIPAMWrite, true, a.deleteZone),
perm(get, "/api/v1/zones/{id}/records", auth.PermIPAMRead, false, a.listRecords),
perm(post, "/api/v1/zones/{id}/records", auth.PermIPAMWrite, true, a.createRecord),
perm(put, "/api/v1/zones/{id}/records/{recordId}", auth.PermIPAMWrite, true, a.updateRecord),
perm(del, "/api/v1/zones/{id}/records/{recordId}", auth.PermIPAMWrite, true, a.deleteRecord),
// Saved filters are personal and allowed in read-only mode.
perm(get, "/api/v1/saved-filters", auth.PermIPAMRead, false, a.listSavedFilters),
perm(post, "/api/v1/saved-filters", auth.PermIPAMRead, false, a.createSavedFilter),
perm(del, "/api/v1/saved-filters/{id}", auth.PermIPAMRead, false, a.deleteSavedFilter),
}
}