package apiv2 import ( "context" "encoding/json" "net/http" "time" "github.com/danielgtaylor/huma/v2" "github.com/juanfont/headscale/hscontrol/scope" "github.com/juanfont/headscale/hscontrol/types" ) func init() { registrations = append(registrations, registerSettings) } // TailnetSettings is the Tailscale tailnet-settings response. Headscale's config // is file-based and mostly not runtime-mutable, so only a few fields carry a // real value; the rest report the default "off". type TailnetSettings struct { ACLsExternallyManagedOn bool `json:"aclsExternallyManagedOn"` ACLsExternalLink string `json:"aclsExternalLink"` DevicesApprovalOn bool `json:"devicesApprovalOn"` DevicesAutoUpdatesOn bool `json:"devicesAutoUpdatesOn"` DevicesKeyDurationDays int `json:"devicesKeyDurationDays"` UsersApprovalOn bool `json:"usersApprovalOn"` UsersRoleAllowedToJoinExternalTailnets string `json:"usersRoleAllowedToJoinExternalTailnets"` NetworkFlowLoggingOn bool `json:"networkFlowLoggingOn"` RegionalRoutingOn bool `json:"regionalRoutingOn"` PostureIdentityCollectionOn bool `json:"postureIdentityCollectionOn"` HTTPSEnabled bool `json:"httpsEnabled"` } type ( getSettingsInput struct { Tailnet string `path:"tailnet"` } settingsOutput struct { Body TailnetSettings } patchSettingsInput struct { Tailnet string `path:"tailnet"` // Accepted and ignored; updating settings is not supported. Body json.RawMessage } ) func registerSettings(api huma.API, b Backend) { settingsTags := []string{"TailnetSettings", "Tailscale compat"} huma.Register(api, requireScope(huma.Operation{ OperationID: "getTailnetSettings", Method: http.MethodGet, Path: "/api/v2/tailnet/{tailnet}/settings", Summary: "Get tailnet settings", Tags: settingsTags, Security: security, Errors: []int{http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound}, }, scope.FeatureSettingsRead), func(ctx context.Context, in *getSettingsInput) (*settingsOutput, error) { err := requireDefaultTailnet(in.Tailnet) if err != nil { return nil, err } cfg := b.Cfg return &settingsOutput{Body: TailnetSettings{ // File-mode policy is genuinely externally managed (read-only via API). ACLsExternallyManagedOn: cfg.Policy.Mode == types.PolicyModeFile, DevicesKeyDurationDays: int(cfg.Node.Expiry / (24 * time.Hour)), HTTPSEnabled: cfg.TLS.CertPath != "" || cfg.TLS.LetsEncrypt.Hostname != "", UsersRoleAllowedToJoinExternalTailnets: "none", }}, nil }) huma.Register(api, requireScope(huma.Operation{ OperationID: "updateTailnetSettings", Method: http.MethodPatch, Path: "/api/v2/tailnet/{tailnet}/settings", Summary: "Update tailnet settings", Tags: settingsTags, Security: security, // The body is accepted but ignored; skip validation. SkipValidateBody: true, Errors: []int{http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusNotImplemented}, }, scope.FeatureSettings), func(ctx context.Context, in *patchSettingsInput) (*settingsOutput, error) { err := requireDefaultTailnet(in.Tailnet) if err != nil { return nil, err } return nil, huma.Error501NotImplemented( "updating tailnet settings is not supported by Headscale", ) }) }