mirror of
https://github.com/juanfont/headscale.git
synced 2026-08-02 13:18:45 +09:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e77a212b1 | |||
| d0b39e7cbb | |||
| add794d190 | |||
| 2a1ae1a2fc | |||
| f636e6db8b | |||
| f6aea642d3 | |||
| 67e0f34527 | |||
| c3d405ce20 | |||
| a22fec9ce1 | |||
| a707201ec3 | |||
| 92048385c2 | |||
| 18f7048c58 | |||
| c386dfe0fd | |||
| deee874bc8 | |||
| bad76ed808 | |||
| a9d5ec6202 | |||
| c1c54f14b6 | |||
| febef2c7cd | |||
| 7526401656 | |||
| 5176dd23a9 | |||
| d955cd6262 | |||
| 33774888c2 | |||
| 8e194721b7 | |||
| 08885c33da | |||
| b988027bb8 | |||
| 112c42af0a | |||
| db6b99d414 | |||
| 8f5e2e7291 | |||
| b5e3b4ee27 | |||
| bbb171d02f | |||
| e1325fbacd | |||
| 29fc14dd73 | |||
| de85965bd8 | |||
| 83fb0a4548 | |||
| fc26c447ec | |||
| df6876a570 | |||
| fe76fe0f57 | |||
| 1eabd70567 | |||
| 562fcc1401 | |||
| 01dcd4011e | |||
| 38dad226c8 |
@@ -28,8 +28,7 @@ jobs:
|
||||
- '*.nix'
|
||||
- 'go.*'
|
||||
- '**/*.go'
|
||||
- '**/*.proto'
|
||||
- 'buf.gen.yaml'
|
||||
- 'openapi/**'
|
||||
- 'tools/**'
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
|
||||
@@ -77,17 +77,3 @@ jobs:
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: nix develop --fallback --command -- prettier --no-error-on-unmatched-pattern
|
||||
--ignore-unknown --check **/*.{ts,js,md,yaml,yml,sass,css,scss,html}
|
||||
|
||||
proto-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
with:
|
||||
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
|
||||
'**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Buf lint
|
||||
run: nix develop --fallback --command -- buf lint proto
|
||||
|
||||
@@ -244,8 +244,6 @@ jobs:
|
||||
- TestACLDynamicUnknownUserRemoval
|
||||
- TestAPIAuthenticationBypass
|
||||
- TestAPIAuthenticationBypassCurl
|
||||
- TestGRPCAuthenticationBypass
|
||||
- TestCLIWithConfigAuthenticationBypass
|
||||
- TestAuthKeyLogoutAndReloginSameUser
|
||||
- TestAuthKeyLogoutAndReloginNewUser
|
||||
- TestAuthKeyLogoutAndReloginSameUserExpiredKey
|
||||
|
||||
@@ -75,9 +75,9 @@ make dev
|
||||
# Individual targets
|
||||
make build # build the headscale binary
|
||||
make test # go test ./...
|
||||
make fmt # format Go, docs, proto
|
||||
make lint # lint Go, proto
|
||||
make generate # regenerate protobuf code (after changes to proto/)
|
||||
make fmt # format Go and docs
|
||||
make lint # lint Go
|
||||
make generate # regenerate ogen code (after changes to openapi/)
|
||||
make clean # remove build artefacts
|
||||
|
||||
# Direct go test invocations
|
||||
@@ -133,9 +133,11 @@ headscale/
|
||||
|
||||
### `hscontrol/` packages
|
||||
|
||||
- `app.go`, `handlers.go`, `grpcv1.go`, `noise.go`, `auth.go`, `oidc.go`,
|
||||
- `app.go`, `handlers.go`, `noise.go`, `auth.go`, `oidc.go`,
|
||||
`poll.go`, `metrics.go`, `debug.go`, `tailsql.go`, `platform_config.go`
|
||||
— top-level server files
|
||||
- `api/v1/` — the ogen-backed v1 HTTP API handlers (`server.go`,
|
||||
`convert.go`, `errors.go`, per-resource handler files)
|
||||
- `state/` — central coordinator (`state.go`) and the copy-on-write
|
||||
`NodeStore` (`node_store.go`). All cross-subsystem operations go
|
||||
through `State`.
|
||||
@@ -209,7 +211,7 @@ both. This is a load-bearing architectural rule.
|
||||
- `SetTags` validation is enforced by `validateNodeOwnership()` in
|
||||
`hscontrol/state/tags.go`.
|
||||
- Examples and edge cases live in `hscontrol/types/node_tags_test.go`
|
||||
and `hscontrol/grpcv1_test.go` (`TestSetTags_*`).
|
||||
and the SetTags tests in `hscontrol/servertest/apiv1_nodes_test.go`.
|
||||
|
||||
**Don't do this**:
|
||||
|
||||
@@ -264,9 +266,9 @@ Key reminders:
|
||||
|
||||
Not Conventional Commits. No `feat:`/`chore:`/`docs:` prefixes.
|
||||
|
||||
- **Protobuf regeneration**: changes under `proto/` require
|
||||
`make generate` (which runs `buf generate`) and should land in a
|
||||
**separate commit** from the callers that use the regenerated types.
|
||||
- **Code generation**: changes under `openapi/` require `make generate`
|
||||
(which runs `ogen`) and should land in a **separate commit** from the
|
||||
callers that use the regenerated types.
|
||||
- **Formatting** is enforced by `golangci-lint` with `golines` (width 88)
|
||||
and `gofumpt`. Run `make fmt` or rely on the pre-commit hook.
|
||||
- **Logging** uses `zerolog`. Prefer single-line chains
|
||||
@@ -286,6 +288,6 @@ Key reminders:
|
||||
- **`.claude/agents/` is deprecated.** Do not create new agent files
|
||||
there. Put behavioural guidance in this file and procedural guidance
|
||||
in the nearest README.
|
||||
- **Do not edit `gen/`** — it is regenerated from `proto/` by
|
||||
- **Do not edit `gen/`** — it is regenerated from `openapi/` by
|
||||
`make generate`.
|
||||
- **Proto changes + code changes should be two commits**, not one.
|
||||
- **Spec changes + code changes should be two commits**, not one.
|
||||
|
||||
@@ -20,7 +20,6 @@ endef
|
||||
|
||||
# Source file collections using shell find for better performance
|
||||
GO_SOURCES := $(shell find . -name '*.go' -not -path './gen/*' -not -path './vendor/*')
|
||||
PROTO_SOURCES := $(shell find . -name '*.proto' -not -path './gen/*' -not -path './vendor/*')
|
||||
PRETTIER_SOURCES := $(shell find . \( -name '*.md' -o -name '*.yaml' -o -name '*.yml' -o -name '*.ts' -o -name '*.js' -o -name '*.html' -o -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -not -path './gen/*' -not -path './vendor/*' -not -path './node_modules/*')
|
||||
|
||||
# Default target
|
||||
@@ -35,8 +34,6 @@ check-deps:
|
||||
$(call check_tool,gofumpt)
|
||||
$(call check_tool,mdformat)
|
||||
$(call check_tool,prettier)
|
||||
$(call check_tool,clang-format)
|
||||
$(call check_tool,buf)
|
||||
|
||||
# Build targets
|
||||
.PHONY: build
|
||||
@@ -53,7 +50,7 @@ test: check-deps $(GO_SOURCES) go.mod go.sum
|
||||
|
||||
# Formatting targets
|
||||
.PHONY: fmt
|
||||
fmt: fmt-go fmt-mdformat fmt-prettier fmt-proto
|
||||
fmt: fmt-go fmt-mdformat fmt-prettier
|
||||
|
||||
.PHONY: fmt-go
|
||||
fmt-go: check-deps $(GO_SOURCES)
|
||||
@@ -71,25 +68,15 @@ fmt-prettier: check-deps $(PRETTIER_SOURCES)
|
||||
@echo "Formatting markup and config files..."
|
||||
prettier --write '**/*.{ts,js,md,yaml,yml,sass,css,scss,html}'
|
||||
|
||||
.PHONY: fmt-proto
|
||||
fmt-proto: check-deps $(PROTO_SOURCES)
|
||||
@echo "Formatting Protocol Buffer files..."
|
||||
clang-format -i $(PROTO_SOURCES)
|
||||
|
||||
# Linting targets
|
||||
.PHONY: lint
|
||||
lint: lint-go lint-proto
|
||||
lint: lint-go
|
||||
|
||||
.PHONY: lint-go
|
||||
lint-go: check-deps $(GO_SOURCES) go.mod go.sum
|
||||
@echo "Linting Go code..."
|
||||
golangci-lint run --timeout 10m
|
||||
|
||||
.PHONY: lint-proto
|
||||
lint-proto: check-deps $(PROTO_SOURCES)
|
||||
@echo "Linting Protocol Buffer files..."
|
||||
cd proto/ && buf lint
|
||||
|
||||
# Code generation
|
||||
.PHONY: generate
|
||||
generate: check-deps
|
||||
@@ -119,9 +106,9 @@ help:
|
||||
@echo " all - Run lint, test, and build (default)"
|
||||
@echo " build - Build headscale binary"
|
||||
@echo " test - Run Go tests"
|
||||
@echo " fmt - Format all code (Go, docs, proto)"
|
||||
@echo " lint - Lint all code (Go, proto)"
|
||||
@echo " generate - Generate code from Protocol Buffers"
|
||||
@echo " fmt - Format all code (Go, docs)"
|
||||
@echo " lint - Lint all code (Go)"
|
||||
@echo " generate - Generate code from the OpenAPI spec"
|
||||
@echo " dev - Full development workflow (fmt + lint + test + build)"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo ""
|
||||
@@ -129,9 +116,7 @@ help:
|
||||
@echo " fmt-go - Format Go code only"
|
||||
@echo " fmt-mdformat - Format documentation only"
|
||||
@echo " fmt-prettier - Format markup and config files only"
|
||||
@echo " fmt-proto - Format Protocol Buffer files only"
|
||||
@echo " lint-go - Lint Go code only"
|
||||
@echo " lint-proto - Lint Protocol Buffer files only"
|
||||
@echo ""
|
||||
@echo "Dependencies:"
|
||||
@echo " check-deps - Verify required tools are available"
|
||||
|
||||
@@ -92,8 +92,7 @@ Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) file.
|
||||
|
||||
### Requirements
|
||||
|
||||
To contribute to headscale you would need the latest version of [Go](https://golang.org)
|
||||
and [Buf](https://buf.build) (Protobuf generator).
|
||||
To contribute to headscale you would need the latest version of [Go](https://golang.org).
|
||||
|
||||
We recommend using [Nix](https://nixos.org/) to setup a development environment. This can
|
||||
be done with `nix develop`, which will install the tools and give you a shell.
|
||||
@@ -110,9 +109,6 @@ formatted with [`golines`](https://github.com/segmentio/golines) (width 88) and
|
||||
Please configure your editor to run the tools while developing and make sure to
|
||||
run `make lint` and `make fmt` before committing any code.
|
||||
|
||||
The **Proto** code is linted with [`buf`](https://docs.buf.build/lint/overview) and
|
||||
formatted with [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html).
|
||||
|
||||
The **docs** are formatted with [`mdformat`](https://mdformat.readthedocs.io).
|
||||
|
||||
The **rest** (Markdown, YAML, etc) is formatted with [`prettier`](https://prettier.io).
|
||||
@@ -122,8 +118,6 @@ Check out the `.golangci.yaml` and `Makefile` to see the specific configuration.
|
||||
### Install development tools
|
||||
|
||||
- Go
|
||||
- Buf
|
||||
- Protobuf tools
|
||||
|
||||
Install and activate:
|
||||
|
||||
@@ -133,8 +127,8 @@ nix develop
|
||||
|
||||
### Testing and building
|
||||
|
||||
Some parts of the project require the generation of Go code from Protobuf
|
||||
(if changes are made in `proto/`) and it must be (re-)generated with:
|
||||
Some parts of the project require generated Go code from the OpenAPI spec
|
||||
(if changes are made in `openapi/`) and it must be (re-)generated with:
|
||||
|
||||
```shell
|
||||
make generate
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
version: v1
|
||||
plugins:
|
||||
- name: go
|
||||
out: gen/go
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- name: go-grpc
|
||||
out: gen/go
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- name: grpc-gateway
|
||||
out: gen/go
|
||||
opt:
|
||||
- paths=source_relative
|
||||
- generate_unbound_methods=true
|
||||
# - name: gorm
|
||||
# out: gen/go
|
||||
# opt:
|
||||
# - paths=source_relative,enums=string,gateway=true
|
||||
- name: openapiv2
|
||||
out: gen/openapiv2
|
||||
+1
-2
@@ -42,8 +42,7 @@ go tool mts node1 status
|
||||
| `--port` | 8080 | Headscale listen port |
|
||||
| `--keep` | false | Keep state directory on exit |
|
||||
|
||||
The metrics/debug port is `port + 1010` (default 9090) and the gRPC
|
||||
port is `port + 42363` (default 50443).
|
||||
The metrics/debug port is `port + 1010` (default 9090).
|
||||
|
||||
## What it does
|
||||
|
||||
|
||||
+9
-11
@@ -28,17 +28,14 @@ var errHealthTimeout = errors.New("health check timed out")
|
||||
|
||||
var errEmptyAuthKey = errors.New("empty auth key in response")
|
||||
|
||||
// maxDevPort is the highest --port value that keeps both the derived
|
||||
// metrics port (port+1010) and gRPC port (port+42363) inside the valid
|
||||
// 1..65535 TCP range.
|
||||
const maxDevPort = 23172
|
||||
// maxDevPort is the highest --port value that keeps the derived metrics port
|
||||
// (port+1010) inside the valid 1..65535 TCP range.
|
||||
const maxDevPort = 64525
|
||||
|
||||
const devConfig = `---
|
||||
server_url: http://127.0.0.1:%d
|
||||
listen_addr: 127.0.0.1:%d
|
||||
metrics_listen_addr: 127.0.0.1:%d
|
||||
grpc_listen_addr: 127.0.0.1:%d
|
||||
grpc_allow_insecure: true
|
||||
|
||||
noise:
|
||||
private_key_path: %s/noise_private.key
|
||||
@@ -83,7 +80,7 @@ func main() {
|
||||
|
||||
if *port < 1 || *port > maxDevPort {
|
||||
log.Fatalf(
|
||||
"--port must be in 1..%d (higher values overflow the derived gRPC port); got %d",
|
||||
"--port must be in 1..%d (higher values overflow the derived metrics port); got %d",
|
||||
maxDevPort, *port,
|
||||
)
|
||||
}
|
||||
@@ -101,7 +98,6 @@ func main() {
|
||||
|
||||
func run() error {
|
||||
metricsPort := *port + 1010 // default 9090
|
||||
grpcPort := *port + 42363 // default 50443
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "headscale-dev-")
|
||||
if err != nil {
|
||||
@@ -114,8 +110,9 @@ func run() error {
|
||||
|
||||
// Write config.
|
||||
configPath := filepath.Join(tmpDir, "config.yaml")
|
||||
configContent := fmt.Sprintf(devConfig,
|
||||
*port, *port, metricsPort, grpcPort,
|
||||
configContent := fmt.Sprintf(
|
||||
devConfig,
|
||||
*port, *port, metricsPort,
|
||||
tmpDir, tmpDir, tmpDir,
|
||||
)
|
||||
|
||||
@@ -193,7 +190,8 @@ func run() error {
|
||||
}
|
||||
|
||||
// Print banner.
|
||||
fmt.Printf(`
|
||||
fmt.Printf(
|
||||
`
|
||||
=== Headscale Dev Environment ===
|
||||
Server: http://127.0.0.1:%d
|
||||
Metrics: http://127.0.0.1:%d
|
||||
|
||||
@@ -2,10 +2,11 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -15,6 +16,8 @@ const (
|
||||
DefaultAPIKeyExpiry = "90d"
|
||||
)
|
||||
|
||||
var errAPIKeyIDNotFound = errors.New("no api key with id")
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(apiKeysCmd)
|
||||
apiKeysCmd.AddCommand(listAPIKeys)
|
||||
@@ -43,26 +46,26 @@ var listAPIKeys = &cobra.Command{
|
||||
Use: cmdList,
|
||||
Short: "List the Api keys for headscale",
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
response, err := client.ListApiKeys(ctx, &v1.ListApiKeysRequest{})
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
resp, err := client.ListApiKeys(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing api keys: %w", err)
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetApiKeys(), func() error {
|
||||
rows := make([][]string, 0, len(response.GetApiKeys()))
|
||||
for _, key := range response.GetApiKeys() {
|
||||
return printListOutput(cmd, resp.ApiKeys, func() error {
|
||||
rows := make([][]string, 0, len(resp.ApiKeys))
|
||||
for _, key := range resp.ApiKeys {
|
||||
expiration := "-"
|
||||
|
||||
if key.GetExpiration() != nil {
|
||||
expiration = ColourTime(key.GetExpiration().AsTime())
|
||||
if key.Expiration.Set {
|
||||
expiration = ColourTime(key.Expiration.Value)
|
||||
}
|
||||
|
||||
rows = append(rows, []string{
|
||||
strconv.FormatUint(key.GetId(), util.Base10),
|
||||
key.GetPrefix(),
|
||||
strconv.FormatUint(key.ID.Value, util.Base10),
|
||||
key.Prefix.Value,
|
||||
expiration,
|
||||
key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
|
||||
key.CreatedAt.Value.Format(HeadscaleDateTimeFormat),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -79,20 +82,20 @@ Creates a new Api key, the Api key is only visible on creation
|
||||
and cannot be retrieved again.
|
||||
If you lose a key, create a new one and revoke (expire) the old one.`,
|
||||
Aliases: []string{"c", cmdNew},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
expiration, err := expirationFromFlag(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := client.CreateApiKey(ctx, &v1.CreateApiKeyRequest{
|
||||
resp, err := client.CreateApiKey(ctx, &apiv1.CreateApiKeyReq{
|
||||
Expiration: expiration,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating api key: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetApiKey(), response.GetApiKey())
|
||||
return printOutput(cmd, resp.ApiKey.Value, resp.ApiKey.Value)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -116,21 +119,21 @@ var expireAPIKeyCmd = &cobra.Command{
|
||||
Use: cmdExpire,
|
||||
Short: "Expire an ApiKey",
|
||||
Aliases: []string{"revoke", aliasExp, "e"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := client.ExpireApiKey(ctx, &v1.ExpireApiKeyRequest{
|
||||
Id: id,
|
||||
Prefix: prefix,
|
||||
err = client.ExpireApiKey(ctx, &apiv1.ExpireApiKeyReq{
|
||||
ID: optUint64(id),
|
||||
Prefix: optString(prefix),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("expiring api key: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Key expired")
|
||||
return printOutput(cmd, map[string]string{colResult: "Key expired"}, "Key expired")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -138,20 +141,42 @@ var deleteAPIKeyCmd = &cobra.Command{
|
||||
Use: cmdDelete,
|
||||
Short: "Delete an ApiKey",
|
||||
Aliases: []string{"remove", aliasDel},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := client.DeleteApiKey(ctx, &v1.DeleteApiKeyRequest{
|
||||
Id: id,
|
||||
Prefix: prefix,
|
||||
})
|
||||
// Delete is routed by prefix in the path, so resolve an --id to its
|
||||
// prefix first; an empty path segment would 404 at the router.
|
||||
if prefix == "" {
|
||||
prefix, err = apiKeyPrefixByID(ctx, client, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = client.DeleteApiKey(ctx, apiv1.DeleteApiKeyParams{Prefix: prefix})
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting api key: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Key deleted")
|
||||
return printOutput(cmd, map[string]string{colResult: "Key deleted"}, "Key deleted")
|
||||
}),
|
||||
}
|
||||
|
||||
// apiKeyPrefixByID looks up an API key's prefix by its numeric ID.
|
||||
func apiKeyPrefixByID(ctx context.Context, client *apiv1.Client, id uint64) (string, error) {
|
||||
resp, err := client.ListApiKeys(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("listing api keys: %w", err)
|
||||
}
|
||||
|
||||
for _, key := range resp.GetApiKeys() {
|
||||
if key.GetID().Or(0) == id {
|
||||
return key.GetPrefix().Or(""), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w: %d", errAPIKeyIDNotFound, id)
|
||||
}
|
||||
|
||||
+19
-22
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -33,45 +33,42 @@ var authCmd = &cobra.Command{
|
||||
var authRegisterCmd = &cobra.Command{
|
||||
Use: "register",
|
||||
Short: "Register a node to your network",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
authID, _ := cmd.Flags().GetString("auth-id")
|
||||
|
||||
request := &v1.AuthRegisterRequest{
|
||||
AuthId: authID,
|
||||
User: user,
|
||||
}
|
||||
|
||||
response, err := client.AuthRegister(ctx, request)
|
||||
resp, err := client.AuthRegister(ctx, &apiv1.AuthRegisterReq{
|
||||
AuthId: apiv1.NewOptString(authID),
|
||||
User: apiv1.NewOptString(user),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("registering node: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(
|
||||
cmd,
|
||||
response.GetNode(),
|
||||
fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName()),
|
||||
resp.Node.Value,
|
||||
fmt.Sprintf("Node %s registered", resp.Node.Value.GivenName.Value),
|
||||
)
|
||||
}),
|
||||
}
|
||||
|
||||
// authDecisionRunE builds a RunE for an auth decision command (approve or
|
||||
// reject) that reads the auth-id flag, invokes the given gRPC call, and prints
|
||||
// the response. errVerb is used in the error message; okMsg is printed on
|
||||
// success.
|
||||
func authDecisionRunE[Resp any](
|
||||
// reject) that reads the auth-id flag, invokes the given API call, and prints a
|
||||
// result. errVerb is used in the error message; okMsg is printed on success.
|
||||
func authDecisionRunE(
|
||||
errVerb, okMsg string,
|
||||
call func(ctx context.Context, client v1.HeadscaleServiceClient, authID string) (Resp, error),
|
||||
call func(ctx context.Context, client *apiv1.Client, authID string) error,
|
||||
) func(*cobra.Command, []string) error {
|
||||
return grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
return apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
authID, _ := cmd.Flags().GetString("auth-id")
|
||||
|
||||
response, err := call(ctx, client, authID)
|
||||
err := call(ctx, client, authID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s auth request: %w", errVerb, err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, okMsg)
|
||||
return printOutput(cmd, map[string]string{colResult: okMsg}, okMsg)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -79,8 +76,8 @@ var authApproveCmd = &cobra.Command{
|
||||
Use: "approve",
|
||||
Short: "Approve a pending authentication request",
|
||||
RunE: authDecisionRunE("approving", "Auth request approved",
|
||||
func(ctx context.Context, client v1.HeadscaleServiceClient, authID string) (*v1.AuthApproveResponse, error) {
|
||||
return client.AuthApprove(ctx, &v1.AuthApproveRequest{AuthId: authID})
|
||||
func(ctx context.Context, client *apiv1.Client, authID string) error {
|
||||
return client.AuthApprove(ctx, &apiv1.AuthApproveReq{AuthId: apiv1.NewOptString(authID)})
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -88,7 +85,7 @@ var authRejectCmd = &cobra.Command{
|
||||
Use: "reject",
|
||||
Short: "Reject a pending authentication request",
|
||||
RunE: authDecisionRunE("rejecting", "Auth request rejected",
|
||||
func(ctx context.Context, client v1.HeadscaleServiceClient, authID string) (*v1.AuthRejectResponse, error) {
|
||||
return client.AuthReject(ctx, &v1.AuthRejectRequest{AuthId: authID})
|
||||
func(ctx context.Context, client *apiv1.Client, authID string) error {
|
||||
return client.AuthReject(ctx, &apiv1.AuthRejectReq{AuthId: apiv1.NewOptString(authID)})
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// apiRunE wraps a cobra [cobra.Command.RunE] func, injecting a ready v1 API
|
||||
// client and context. Connection lifecycle is managed by the wrapper.
|
||||
func apiRunE(
|
||||
fn func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error,
|
||||
) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
ctx, client, cancel, err := newHeadscaleAPIClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
return fn(ctx, client, cmd, args)
|
||||
}
|
||||
}
|
||||
|
||||
// withAPI opens a v1 API client, runs fn with it, and cancels the context
|
||||
// afterwards. It is the building block for commands that branch on a flag
|
||||
// before deciding to talk to the server.
|
||||
func withAPI(fn func(ctx context.Context, client *apiv1.Client) error) error {
|
||||
ctx, client, cancel, err := newHeadscaleAPIClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
return fn(ctx, client)
|
||||
}
|
||||
|
||||
// newHeadscaleAPIClient builds a v1 HTTP API client. With no configured
|
||||
// address it talks to the local unix socket (filesystem permissions are the
|
||||
// trust boundary, no API key needed); otherwise it uses HTTPS with the API key.
|
||||
func newHeadscaleAPIClient() (context.Context, *apiv1.Client, context.CancelFunc, error) {
|
||||
cfg, err := types.LoadCLIConfig()
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("loading configuration: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.CLI.Timeout)
|
||||
|
||||
if cfg.CLI.Address == "" {
|
||||
client, err := localSocketClient(cfg.UnixSocket)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return ctx, client, cancel, nil
|
||||
}
|
||||
|
||||
client, err := remoteClient(cfg.CLI.Address, cfg.CLI.APIKey, cfg.CLI.Insecure)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return ctx, client, cancel, nil
|
||||
}
|
||||
|
||||
func localSocketClient(socketPath string) (*apiv1.Client, error) {
|
||||
err := checkSocketPermissions(socketPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return dialSocketWaiting(ctx, socketPath)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// The socket bypasses bearer auth; the token is a placeholder.
|
||||
return apiv1.NewClient("http://unix", cliToken("local-socket"), apiv1.WithClient(httpClient))
|
||||
}
|
||||
|
||||
// dialSocketWaiting connects to the local unix socket, retrying while it is
|
||||
// still absent. headscale removes and rebinds the socket during startup, so a
|
||||
// CLI command issued right after "systemctl start" can beat the server to it.
|
||||
// This mirrors the old blocking dial, which retried until the CLI timeout.
|
||||
func dialSocketWaiting(ctx context.Context, socketPath string) (net.Conn, error) {
|
||||
var dialer net.Dialer
|
||||
|
||||
for {
|
||||
conn, err := dialer.DialContext(ctx, "unix", socketPath)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, err
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func remoteClient(address, apiKey string, insecure bool) (*apiv1.Client, error) {
|
||||
if apiKey == "" {
|
||||
return nil, errAPIKeyNotSet
|
||||
}
|
||||
|
||||
transport := &http.Transport{}
|
||||
if insecure {
|
||||
//nolint:gosec // G402: insecure is an explicit, documented opt-in.
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
return apiv1.NewClient(
|
||||
serverURLFromAddress(address),
|
||||
cliToken(apiKey),
|
||||
apiv1.WithClient(&http.Client{Transport: transport}),
|
||||
)
|
||||
}
|
||||
|
||||
// serverURLFromAddress turns a configured CLI address into a base URL,
|
||||
// defaulting to https when no scheme is given.
|
||||
func serverURLFromAddress(address string) string {
|
||||
if strings.Contains(address, "://") {
|
||||
return address
|
||||
}
|
||||
|
||||
return "https://" + address
|
||||
}
|
||||
|
||||
// checkSocketPermissions gives a friendlier error than a dial failure when the
|
||||
// user cannot access the headscale socket.
|
||||
func checkSocketPermissions(socketPath string) error {
|
||||
socket, err := os.OpenFile(socketPath, os.O_WRONLY, SocketWritePermissions) //nolint
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
return fmt.Errorf(
|
||||
"unable to read/write to headscale socket %q, do you have the correct permissions? %w",
|
||||
socketPath, err,
|
||||
)
|
||||
}
|
||||
|
||||
// ENXIO and similar are expected for a socket opened with O_WRONLY; the
|
||||
// real connection uses net.Dial which handles sockets properly.
|
||||
return nil
|
||||
}
|
||||
|
||||
socket.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// optString / optUint64 / optTime build optional API request values from flag
|
||||
// inputs, treating zero values as "unset".
|
||||
|
||||
func optString(s string) apiv1.OptString {
|
||||
if s == "" {
|
||||
return apiv1.OptString{}
|
||||
}
|
||||
|
||||
return apiv1.NewOptString(s)
|
||||
}
|
||||
|
||||
func optUint64(v uint64) apiv1.OptUint64 {
|
||||
if v == 0 {
|
||||
return apiv1.OptUint64{}
|
||||
}
|
||||
|
||||
return apiv1.NewOptUint64(v)
|
||||
}
|
||||
|
||||
// expirationFromFlag parses the --expiration flag as a Prometheus-style
|
||||
// duration (e.g. "90d", "1h") and returns it as an absolute optional timestamp.
|
||||
// An empty flag yields an unset value.
|
||||
func expirationFromFlag(cmd *cobra.Command) (apiv1.OptDateTime, error) {
|
||||
durationStr, _ := cmd.Flags().GetString("expiration")
|
||||
if durationStr == "" {
|
||||
return apiv1.OptDateTime{}, nil
|
||||
}
|
||||
|
||||
duration, err := model.ParseDuration(durationStr)
|
||||
if err != nil {
|
||||
return apiv1.OptDateTime{}, fmt.Errorf("parsing duration: %w", err)
|
||||
}
|
||||
|
||||
return apiv1.NewOptDateTime(time.Now().UTC().Add(time.Duration(duration))), nil
|
||||
}
|
||||
|
||||
// cliToken is an [apiv1.SecuritySource] that supplies a fixed bearer token.
|
||||
type cliToken string
|
||||
|
||||
func (t cliToken) BearerAuth(context.Context, apiv1.OperationName) (apiv1.BearerAuth, error) {
|
||||
return apiv1.BearerAuth{Token: string(t)}, nil
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -32,7 +32,7 @@ var debugCmd = &cobra.Command{
|
||||
var createNodeCmd = &cobra.Command{
|
||||
Use: "create-node",
|
||||
Short: "Create a node that can be registered with `auth register <>` command",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
registrationID, _ := cmd.Flags().GetString("key")
|
||||
@@ -44,18 +44,16 @@ var createNodeCmd = &cobra.Command{
|
||||
|
||||
routes, _ := cmd.Flags().GetStringSlice("route")
|
||||
|
||||
request := &v1.DebugCreateNodeRequest{
|
||||
Key: registrationID,
|
||||
Name: name,
|
||||
User: user,
|
||||
resp, err := client.DebugCreateNode(ctx, &apiv1.DebugCreateNodeReq{
|
||||
Key: apiv1.NewOptString(registrationID),
|
||||
Name: apiv1.NewOptString(name),
|
||||
User: apiv1.NewOptString(user),
|
||||
Routes: routes,
|
||||
}
|
||||
|
||||
response, err := client.DebugCreateNode(ctx, request)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating node: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetNode(), "Node created")
|
||||
return printOutput(cmd, resp.Node.Value, "Node created")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -16,12 +16,12 @@ var healthCmd = &cobra.Command{
|
||||
Use: "health",
|
||||
Short: "Check the health of the Headscale server",
|
||||
Long: "Check the health of the Headscale server. This command will return an exit code of 0 if the server is healthy, or 1 if it is not.",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
response, err := client.Health(ctx, &v1.HealthRequest{})
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
resp, err := client.Health(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking health: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "")
|
||||
return printOutput(cmd, resp, "")
|
||||
}),
|
||||
}
|
||||
|
||||
+92
-151
@@ -8,12 +8,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
@@ -67,24 +66,22 @@ var registerNodeCmd = &cobra.Command{
|
||||
Use: "register",
|
||||
Short: "Registers a node to your network",
|
||||
Deprecated: "use 'headscale auth register --auth-id <id> --user <user>' instead",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
registrationID, _ := cmd.Flags().GetString("key")
|
||||
|
||||
request := &v1.RegisterNodeRequest{
|
||||
Key: registrationID,
|
||||
User: user,
|
||||
}
|
||||
|
||||
response, err := client.RegisterNode(ctx, request)
|
||||
resp, err := client.RegisterNode(ctx, apiv1.RegisterNodeParams{
|
||||
Key: optString(registrationID),
|
||||
User: optString(user),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("registering node: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(
|
||||
cmd,
|
||||
response.GetNode(),
|
||||
fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName()),
|
||||
resp.Node.Value,
|
||||
fmt.Sprintf("Node %s registered", resp.Node.Value.GivenName.Value),
|
||||
)
|
||||
}),
|
||||
}
|
||||
@@ -93,16 +90,16 @@ var listNodesCmd = &cobra.Command{
|
||||
Use: cmdList,
|
||||
Short: "List nodes",
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
|
||||
response, err := client.ListNodes(ctx, &v1.ListNodesRequest{User: user})
|
||||
resp, err := client.ListNodes(ctx, apiv1.ListNodesParams{User: optString(user)})
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing nodes: %w", err)
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetNodes(), func() error {
|
||||
tableData, err := nodesToPtables(response.GetNodes())
|
||||
return printListOutput(cmd, resp.Nodes, func() error {
|
||||
tableData, err := nodesToPtables(resp.Nodes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("converting to table: %w", err)
|
||||
}
|
||||
@@ -116,27 +113,27 @@ var listNodeRoutesCmd = &cobra.Command{
|
||||
Use: "list-routes",
|
||||
Short: "List routes available on nodes",
|
||||
Aliases: []string{"lsr", "routes"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
|
||||
response, err := client.ListNodes(ctx, &v1.ListNodesRequest{})
|
||||
resp, err := client.ListNodes(ctx, apiv1.ListNodesParams{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing nodes: %w", err)
|
||||
}
|
||||
|
||||
nodes := response.GetNodes()
|
||||
nodes := resp.Nodes
|
||||
if identifier != 0 {
|
||||
for _, node := range response.GetNodes() {
|
||||
if node.GetId() == identifier {
|
||||
nodes = []*v1.Node{node}
|
||||
for _, node := range resp.Nodes {
|
||||
if node.ID.Value == identifier {
|
||||
nodes = []apiv1.Node{node}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodes = lo.Filter(nodes, func(n *v1.Node, _ int) bool {
|
||||
return len(n.GetSubnetRoutes()) > 0 || len(n.GetApprovedRoutes()) > 0 || len(n.GetAvailableRoutes()) > 0
|
||||
nodes = lo.Filter(nodes, func(n apiv1.Node, _ int) bool {
|
||||
return len(n.SubnetRoutes) > 0 || len(n.ApprovedRoutes) > 0 || len(n.AvailableRoutes) > 0
|
||||
})
|
||||
|
||||
return printListOutput(cmd, nodes, func() error {
|
||||
@@ -152,23 +149,21 @@ var expireNodeCmd = &cobra.Command{
|
||||
|
||||
Use --disable to disable key expiry (node will never expire).`,
|
||||
Aliases: []string{"logout", aliasExp, "e"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
disableExpiry, _ := cmd.Flags().GetBool("disable")
|
||||
|
||||
// Handle disable expiry - node will never expire.
|
||||
if disableExpiry {
|
||||
request := &v1.ExpireNodeRequest{
|
||||
NodeId: identifier,
|
||||
DisableExpiry: true,
|
||||
}
|
||||
|
||||
response, err := client.ExpireNode(ctx, request)
|
||||
resp, err := client.ExpireNode(ctx, apiv1.ExpireNodeParams{
|
||||
NodeID: identifier,
|
||||
DisableExpiry: apiv1.NewOptBool(true),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("disabling node expiry: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetNode(), "Node expiry disabled")
|
||||
return printOutput(cmd, resp.Node.Value, "Node expiry disabled")
|
||||
}
|
||||
|
||||
expiry, _ := cmd.Flags().GetString("expiry")
|
||||
@@ -186,28 +181,26 @@ Use --disable to disable key expiry (node will never expire).`,
|
||||
}
|
||||
}
|
||||
|
||||
request := &v1.ExpireNodeRequest{
|
||||
NodeId: identifier,
|
||||
Expiry: timestamppb.New(expiryTime),
|
||||
}
|
||||
|
||||
response, err := client.ExpireNode(ctx, request)
|
||||
resp, err := client.ExpireNode(ctx, apiv1.ExpireNodeParams{
|
||||
NodeID: identifier,
|
||||
Expiry: apiv1.NewOptDateTime(expiryTime),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("expiring node: %w", err)
|
||||
}
|
||||
|
||||
if now.Equal(expiryTime) || now.After(expiryTime) {
|
||||
return printOutput(cmd, response.GetNode(), "Node expired")
|
||||
return printOutput(cmd, resp.Node.Value, "Node expired")
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetNode(), "Node expiration updated")
|
||||
return printOutput(cmd, resp.Node.Value, "Node expiration updated")
|
||||
}),
|
||||
}
|
||||
|
||||
var renameNodeCmd = &cobra.Command{
|
||||
Use: "rename NEW_NAME",
|
||||
Short: "Renames a node in your network",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
|
||||
newName := ""
|
||||
@@ -215,17 +208,15 @@ var renameNodeCmd = &cobra.Command{
|
||||
newName = args[0]
|
||||
}
|
||||
|
||||
request := &v1.RenameNodeRequest{
|
||||
NodeId: identifier,
|
||||
resp, err := client.RenameNode(ctx, apiv1.RenameNodeParams{
|
||||
NodeID: identifier,
|
||||
NewName: newName,
|
||||
}
|
||||
|
||||
response, err := client.RenameNode(ctx, request)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("renaming node: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetNode(), "Node renamed")
|
||||
return printOutput(cmd, resp.Node.Value, "Node renamed")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -233,30 +224,22 @@ var deleteNodeCmd = &cobra.Command{
|
||||
Use: cmdDelete,
|
||||
Short: "Delete a node",
|
||||
Aliases: []string{aliasDel},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
|
||||
getRequest := &v1.GetNodeRequest{
|
||||
NodeId: identifier,
|
||||
}
|
||||
|
||||
getResponse, err := client.GetNode(ctx, getRequest)
|
||||
getResp, err := client.GetNode(ctx, apiv1.GetNodeParams{NodeID: identifier})
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting node: %w", err)
|
||||
}
|
||||
|
||||
deleteRequest := &v1.DeleteNodeRequest{
|
||||
NodeId: identifier,
|
||||
}
|
||||
|
||||
if !confirmAction(cmd, fmt.Sprintf(
|
||||
"Do you want to remove the node %s?",
|
||||
getResponse.GetNode().GetName(),
|
||||
getResp.Node.Value.Name.Value,
|
||||
)) {
|
||||
return printOutput(cmd, map[string]string{colResult: "Node not deleted"}, "Node not deleted")
|
||||
}
|
||||
|
||||
_, err = client.DeleteNode(ctx, deleteRequest)
|
||||
err = client.DeleteNode(ctx, apiv1.DeleteNodeParams{NodeID: identifier})
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting node: %w", err)
|
||||
}
|
||||
@@ -284,28 +267,23 @@ all nodes that are missing.
|
||||
If you remove IPv4 or IPv6 prefixes from the config,
|
||||
it can be run to remove the IPs that should no longer
|
||||
be assigned to nodes.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
if !confirmAction(cmd, "Are you sure that you want to assign/remove IPs to/from nodes?") {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
|
||||
changes, err := client.BackfillNodeIPs(ctx, &v1.BackfillNodeIPsRequest{Confirmed: true})
|
||||
resp, err := client.BackfillNodeIPs(ctx, apiv1.BackfillNodeIPsParams{
|
||||
Confirmed: apiv1.NewOptBool(true),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("backfilling IPs: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, changes, "Node IPs backfilled successfully")
|
||||
},
|
||||
return printOutput(cmd, resp.Changes, "Node IPs backfilled successfully")
|
||||
}),
|
||||
}
|
||||
|
||||
func nodesToPtables(nodes []*v1.Node) (pterm.TableData, error) {
|
||||
func nodesToPtables(nodes []apiv1.Node) (pterm.TableData, error) {
|
||||
tableHeader := []string{
|
||||
"ID",
|
||||
"Hostname",
|
||||
@@ -325,75 +303,52 @@ func nodesToPtables(nodes []*v1.Node) (pterm.TableData, error) {
|
||||
tableData[0] = tableHeader
|
||||
|
||||
for _, node := range nodes {
|
||||
var ephemeral bool
|
||||
if node.GetPreAuthKey() != nil && node.GetPreAuthKey().GetEphemeral() {
|
||||
ephemeral = true
|
||||
ephemeral := node.PreAuthKey.Set && node.PreAuthKey.Value.Ephemeral.Value
|
||||
|
||||
var lastSeenTime string
|
||||
if node.LastSeen.Set {
|
||||
lastSeenTime = node.LastSeen.Value.Format(HeadscaleDateTimeFormat)
|
||||
}
|
||||
|
||||
var (
|
||||
lastSeen time.Time
|
||||
lastSeenTime string
|
||||
)
|
||||
|
||||
if node.GetLastSeen() != nil {
|
||||
lastSeen = node.GetLastSeen().AsTime()
|
||||
lastSeenTime = lastSeen.Format(HeadscaleDateTimeFormat)
|
||||
}
|
||||
|
||||
var (
|
||||
expiry time.Time
|
||||
expiryTime string
|
||||
)
|
||||
|
||||
if node.GetExpiry() != nil {
|
||||
expiry = node.GetExpiry().AsTime()
|
||||
expiryTime = expiry.Format(HeadscaleDateTimeFormat)
|
||||
} else {
|
||||
expiryTime = "N/A"
|
||||
expiryTime := "N/A"
|
||||
if node.Expiry.Set {
|
||||
expiryTime = node.Expiry.Value.Format(HeadscaleDateTimeFormat)
|
||||
}
|
||||
|
||||
var machineKey key.MachinePublic
|
||||
|
||||
err := machineKey.UnmarshalText(
|
||||
[]byte(node.GetMachineKey()),
|
||||
)
|
||||
err := machineKey.UnmarshalText([]byte(node.MachineKey.Value))
|
||||
if err != nil {
|
||||
machineKey = key.MachinePublic{}
|
||||
}
|
||||
|
||||
var nodeKey key.NodePublic
|
||||
|
||||
err = nodeKey.UnmarshalText(
|
||||
[]byte(node.GetNodeKey()),
|
||||
)
|
||||
err = nodeKey.UnmarshalText([]byte(node.NodeKey.Value))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var online string
|
||||
if node.GetOnline() {
|
||||
online := pterm.LightRed("offline")
|
||||
if node.Online.Value {
|
||||
online = pterm.LightGreen("online")
|
||||
} else {
|
||||
online = pterm.LightRed("offline")
|
||||
}
|
||||
|
||||
var expired string
|
||||
if node.GetExpiry() != nil && node.GetExpiry().AsTime().Before(time.Now()) {
|
||||
expired := pterm.LightGreen("no")
|
||||
if node.Expiry.Set && node.Expiry.Value.Before(time.Now()) {
|
||||
expired = pterm.LightRed("yes")
|
||||
} else {
|
||||
expired = pterm.LightGreen("no")
|
||||
}
|
||||
|
||||
tags := strings.Join(node.GetTags(), "\n")
|
||||
tags := strings.Join(node.Tags, "\n")
|
||||
|
||||
var user string
|
||||
if node.GetUser() != nil {
|
||||
user = node.GetUser().GetName()
|
||||
if node.User.Set {
|
||||
user = node.User.Value.Name.Value
|
||||
}
|
||||
|
||||
var ipBuilder strings.Builder
|
||||
|
||||
for _, addr := range node.GetIpAddresses() {
|
||||
for _, addr := range node.IpAddresses {
|
||||
ip, err := netip.ParseAddr(addr)
|
||||
if err == nil {
|
||||
if ipBuilder.Len() > 0 {
|
||||
@@ -404,35 +359,28 @@ func nodesToPtables(nodes []*v1.Node) (pterm.TableData, error) {
|
||||
}
|
||||
}
|
||||
|
||||
ipAddresses := ipBuilder.String()
|
||||
|
||||
nodeData := []string{
|
||||
strconv.FormatUint(node.GetId(), util.Base10),
|
||||
node.GetName(),
|
||||
node.GetGivenName(),
|
||||
strconv.FormatUint(node.ID.Value, util.Base10),
|
||||
node.Name.Value,
|
||||
node.GivenName.Value,
|
||||
machineKey.ShortString(),
|
||||
nodeKey.ShortString(),
|
||||
user,
|
||||
tags,
|
||||
ipAddresses,
|
||||
ipBuilder.String(),
|
||||
strconv.FormatBool(ephemeral),
|
||||
lastSeenTime,
|
||||
expiryTime,
|
||||
online,
|
||||
expired,
|
||||
}
|
||||
tableData = append(
|
||||
tableData,
|
||||
nodeData,
|
||||
)
|
||||
tableData = append(tableData, nodeData)
|
||||
}
|
||||
|
||||
return tableData, nil
|
||||
}
|
||||
|
||||
func nodeRoutesToPtables(
|
||||
nodes []*v1.Node,
|
||||
) pterm.TableData {
|
||||
func nodeRoutesToPtables(nodes []apiv1.Node) pterm.TableData {
|
||||
tableHeader := []string{
|
||||
"ID",
|
||||
"Hostname",
|
||||
@@ -445,16 +393,13 @@ func nodeRoutesToPtables(
|
||||
|
||||
for _, node := range nodes {
|
||||
nodeData := []string{
|
||||
strconv.FormatUint(node.GetId(), util.Base10),
|
||||
node.GetGivenName(),
|
||||
strings.Join(node.GetApprovedRoutes(), "\n"),
|
||||
strings.Join(node.GetAvailableRoutes(), "\n"),
|
||||
strings.Join(node.GetSubnetRoutes(), "\n"),
|
||||
strconv.FormatUint(node.ID.Value, util.Base10),
|
||||
node.GivenName.Value,
|
||||
strings.Join(node.ApprovedRoutes, "\n"),
|
||||
strings.Join(node.AvailableRoutes, "\n"),
|
||||
strings.Join(node.SubnetRoutes, "\n"),
|
||||
}
|
||||
tableData = append(
|
||||
tableData,
|
||||
nodeData,
|
||||
)
|
||||
tableData = append(tableData, nodeData)
|
||||
}
|
||||
|
||||
return tableData
|
||||
@@ -464,43 +409,39 @@ var tagCmd = &cobra.Command{
|
||||
Use: "tag",
|
||||
Short: "Manage the tags of a node",
|
||||
Aliases: []string{"tags", "t"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
tagsToSet, _ := cmd.Flags().GetStringSlice("tags")
|
||||
|
||||
// Sending tags to node
|
||||
request := &v1.SetTagsRequest{
|
||||
NodeId: identifier,
|
||||
Tags: tagsToSet,
|
||||
}
|
||||
|
||||
resp, err := client.SetTags(ctx, request)
|
||||
resp, err := client.SetTags(
|
||||
ctx,
|
||||
&apiv1.SetTagsReq{Tags: tagsToSet},
|
||||
apiv1.SetTagsParams{NodeID: identifier},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting tags: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.GetNode(), "Node updated")
|
||||
return printOutput(cmd, resp.Node.Value, "Node updated")
|
||||
}),
|
||||
}
|
||||
|
||||
var approveRoutesCmd = &cobra.Command{
|
||||
Use: "approve-routes",
|
||||
Short: "Manage the approved routes of a node",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
routes, _ := cmd.Flags().GetStringSlice("routes")
|
||||
|
||||
// Sending routes to node
|
||||
request := &v1.SetApprovedRoutesRequest{
|
||||
NodeId: identifier,
|
||||
Routes: routes,
|
||||
}
|
||||
|
||||
resp, err := client.SetApprovedRoutes(ctx, request)
|
||||
resp, err := client.SetApprovedRoutes(
|
||||
ctx,
|
||||
&apiv1.SetApprovedRoutesReq{Routes: routes},
|
||||
apiv1.SetApprovedRoutesParams{NodeID: identifier},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting approved routes: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.GetNode(), "Node updated")
|
||||
return printOutput(cmd, resp.Node.Value, "Node updated")
|
||||
}),
|
||||
}
|
||||
|
||||
+19
-19
@@ -6,7 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/db"
|
||||
"github.com/juanfont/headscale/hscontrol/policy"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
@@ -15,13 +15,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
bypassFlag = "bypass-grpc-and-access-database-directly" //nolint:gosec // not a credential
|
||||
bypassFlag = "bypass-server-and-access-database-directly" //nolint:gosec // not a credential
|
||||
)
|
||||
|
||||
var errAborted = errors.New("command aborted by user")
|
||||
|
||||
// bypassDatabase loads the server config and opens the database directly,
|
||||
// bypassing the gRPC server. The caller is responsible for closing the
|
||||
// bypassing the running server. The caller is responsible for closing the
|
||||
// returned database handle.
|
||||
func bypassDatabase() (*db.HSDatabase, error) {
|
||||
cfg, err := types.LoadServerConfig()
|
||||
@@ -50,16 +50,16 @@ func openBypassDB(cmd *cobra.Command) (*db.HSDatabase, error) {
|
||||
func init() {
|
||||
rootCmd.AddCommand(policyCmd)
|
||||
|
||||
getPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing gRPC and does not require the server to be running")
|
||||
getPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing the API and does not require the server to be running")
|
||||
policyCmd.AddCommand(getPolicy)
|
||||
|
||||
setPolicy.Flags().StringP("file", "f", "", "Path to a policy file in HuJSON format")
|
||||
setPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing gRPC and does not require the server to be running")
|
||||
setPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing the API and does not require the server to be running")
|
||||
mustMarkRequired(setPolicy, "file")
|
||||
policyCmd.AddCommand(setPolicy)
|
||||
|
||||
checkPolicy.Flags().StringP("file", "f", "", "Path to a policy file in HuJSON format")
|
||||
checkPolicy.Flags().BoolP(bypassFlag, "", false, "Open the database directly (no gRPC, no running server) to resolve user references and to evaluate the policy's tests and sshTests blocks. Required when those checks are needed.")
|
||||
checkPolicy.Flags().BoolP(bypassFlag, "", false, "Open the database directly (no running server required) to resolve user references and to evaluate the policy's tests and sshTests blocks. Required when those checks are needed.")
|
||||
mustMarkRequired(checkPolicy, "file")
|
||||
policyCmd.AddCommand(checkPolicy)
|
||||
}
|
||||
@@ -90,13 +90,13 @@ var getPolicy = &cobra.Command{
|
||||
|
||||
policyData = pol.Data
|
||||
} else {
|
||||
err := withGRPC(func(ctx context.Context, client v1.HeadscaleServiceClient) error {
|
||||
response, err := client.GetPolicy(ctx, &v1.GetPolicyRequest{})
|
||||
err := withAPI(func(ctx context.Context, client *apiv1.Client) error {
|
||||
resp, err := client.GetPolicy(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading ACL policy: %w", err)
|
||||
}
|
||||
|
||||
policyData = response.GetPolicy()
|
||||
policyData = resp.Policy.Value
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -150,10 +150,10 @@ var setPolicy = &cobra.Command{
|
||||
return fmt.Errorf("setting ACL policy: %w", err)
|
||||
}
|
||||
} else {
|
||||
request := &v1.SetPolicyRequest{Policy: string(policyBytes)}
|
||||
|
||||
err := withGRPC(func(ctx context.Context, client v1.HeadscaleServiceClient) error {
|
||||
_, err := client.SetPolicy(ctx, request)
|
||||
err := withAPI(func(ctx context.Context, client *apiv1.Client) error {
|
||||
_, err := client.SetPolicy(ctx, &apiv1.SetPolicyReq{
|
||||
Policy: apiv1.NewOptString(string(policyBytes)),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting ACL policy: %w", err)
|
||||
}
|
||||
@@ -176,8 +176,8 @@ var checkPolicy = &cobra.Command{
|
||||
Short: "Check the Policy file for errors",
|
||||
Long: `
|
||||
Check validates the policy against the server's live users and nodes,
|
||||
running any "tests" or "sshTests" block. By default the command is a
|
||||
thin frontend for a gRPC call to a running headscale; pass --` + bypassFlag + ` to
|
||||
running any "tests" or "sshTests" block. By default the command calls a
|
||||
running headscale over its API; pass --` + bypassFlag + ` to
|
||||
open the database directly when headscale is not running.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
policyPath, _ := cmd.Flags().GetString("file")
|
||||
@@ -223,10 +223,10 @@ var checkPolicy = &cobra.Command{
|
||||
return nil
|
||||
}
|
||||
|
||||
err = withGRPC(func(ctx context.Context, client v1.HeadscaleServiceClient) error {
|
||||
_, err := client.CheckPolicy(ctx, &v1.CheckPolicyRequest{Policy: string(policyBytes)})
|
||||
|
||||
return err
|
||||
err = withAPI(func(ctx context.Context, client *apiv1.Client) error {
|
||||
return client.CheckPolicy(ctx, &apiv1.CheckPolicyReq{
|
||||
Policy: apiv1.NewOptString(string(policyBytes)),
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -44,37 +44,39 @@ var listPreAuthKeys = &cobra.Command{
|
||||
Use: cmdList,
|
||||
Short: "List all preauthkeys",
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
response, err := client.ListPreAuthKeys(ctx, &v1.ListPreAuthKeysRequest{})
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
resp, err := client.ListPreAuthKeys(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing preauthkeys: %w", err)
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetPreAuthKeys(), func() error {
|
||||
rows := make([][]string, 0, len(response.GetPreAuthKeys()))
|
||||
for _, key := range response.GetPreAuthKeys() {
|
||||
return printListOutput(cmd, resp.PreAuthKeys, func() error {
|
||||
rows := make([][]string, 0, len(resp.PreAuthKeys))
|
||||
for _, key := range resp.PreAuthKeys {
|
||||
expiration := "-"
|
||||
if key.GetExpiration() != nil {
|
||||
expiration = ColourTime(key.GetExpiration().AsTime())
|
||||
if key.Expiration.Set {
|
||||
expiration = ColourTime(key.Expiration.Value)
|
||||
}
|
||||
|
||||
var owner string
|
||||
if len(key.GetAclTags()) > 0 {
|
||||
owner = strings.Join(key.GetAclTags(), "\n")
|
||||
} else if key.GetUser() != nil {
|
||||
owner = key.GetUser().GetName()
|
||||
} else {
|
||||
|
||||
switch {
|
||||
case len(key.AclTags) > 0:
|
||||
owner = strings.Join(key.AclTags, "\n")
|
||||
case key.User.Set:
|
||||
owner = key.User.Value.Name.Value
|
||||
default:
|
||||
owner = "-"
|
||||
}
|
||||
|
||||
rows = append(rows, []string{
|
||||
strconv.FormatUint(key.GetId(), util.Base10),
|
||||
key.GetKey(),
|
||||
strconv.FormatBool(key.GetReusable()),
|
||||
strconv.FormatBool(key.GetEphemeral()),
|
||||
strconv.FormatBool(key.GetUsed()),
|
||||
strconv.FormatUint(key.ID.Value, util.Base10),
|
||||
key.Key.Value,
|
||||
strconv.FormatBool(key.Reusable.Value),
|
||||
strconv.FormatBool(key.Ephemeral.Value),
|
||||
strconv.FormatBool(key.Used.Value),
|
||||
expiration,
|
||||
key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
|
||||
key.CreatedAt.Value.Format(HeadscaleDateTimeFormat),
|
||||
owner,
|
||||
})
|
||||
}
|
||||
@@ -97,7 +99,7 @@ var createPreAuthKeyCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Creates a new preauthkey",
|
||||
Aliases: []string{"c", cmdNew},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetUint64("user")
|
||||
reusable, _ := cmd.Flags().GetBool("reusable")
|
||||
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
|
||||
@@ -108,20 +110,18 @@ var createPreAuthKeyCmd = &cobra.Command{
|
||||
return err
|
||||
}
|
||||
|
||||
request := &v1.CreatePreAuthKeyRequest{
|
||||
User: user,
|
||||
Reusable: reusable,
|
||||
Ephemeral: ephemeral,
|
||||
resp, err := client.CreatePreAuthKey(ctx, &apiv1.CreatePreAuthKeyReq{
|
||||
User: optUint64(user),
|
||||
Reusable: apiv1.NewOptBool(reusable),
|
||||
Ephemeral: apiv1.NewOptBool(ephemeral),
|
||||
AclTags: tags,
|
||||
Expiration: expiration,
|
||||
}
|
||||
|
||||
response, err := client.CreatePreAuthKey(ctx, request)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating preauthkey: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetPreAuthKey(), response.GetPreAuthKey().GetKey())
|
||||
return printOutput(cmd, resp.PreAuthKey.Value, resp.PreAuthKey.Value.Key.Value)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -139,22 +139,18 @@ var expirePreAuthKeyCmd = &cobra.Command{
|
||||
Use: cmdExpire,
|
||||
Short: "Expire a preauthkey",
|
||||
Aliases: []string{"revoke", aliasExp, "e"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
id, err := preAuthKeyID(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := &v1.ExpirePreAuthKeyRequest{
|
||||
Id: id,
|
||||
}
|
||||
|
||||
response, err := client.ExpirePreAuthKey(ctx, request)
|
||||
err = client.ExpirePreAuthKey(ctx, &apiv1.ExpirePreAuthKeyReq{ID: apiv1.NewOptUint64(id)})
|
||||
if err != nil {
|
||||
return fmt.Errorf("expiring preauthkey: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Key expired")
|
||||
return printOutput(cmd, map[string]string{colResult: "Key expired"}, "Key expired")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -162,21 +158,17 @@ var deletePreAuthKeyCmd = &cobra.Command{
|
||||
Use: cmdDelete,
|
||||
Short: "Delete a preauthkey",
|
||||
Aliases: []string{aliasDel, "rm", "d"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
id, err := preAuthKeyID(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := &v1.DeletePreAuthKeyRequest{
|
||||
Id: id,
|
||||
}
|
||||
|
||||
response, err := client.DeletePreAuthKey(ctx, request)
|
||||
err = client.DeletePreAuthKey(ctx, apiv1.DeletePreAuthKeyParams{ID: apiv1.NewOptUint64(id)})
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting preauthkey: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Key deleted")
|
||||
return printOutput(cmd, map[string]string{colResult: "Key deleted"}, "Key deleted")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -213,7 +213,8 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := filterPreReleasesIfStable(func() string { return tt.currentVersion })(tt.tag)
|
||||
if result != tt.expectedFilter {
|
||||
t.Errorf("%s: got %v, want %v\nDescription: %s\nCurrent version: %s, Tag: %s",
|
||||
t.Errorf(
|
||||
"%s: got %v, want %v\nDescription: %s\nCurrent version: %s, Tag: %s",
|
||||
tt.name,
|
||||
result,
|
||||
tt.expectedFilter,
|
||||
@@ -293,7 +294,8 @@ func TestIsPreReleaseVersion(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isPreReleaseVersion(tt.version)
|
||||
if result != tt.expected {
|
||||
t.Errorf("%s: got %v, want %v\nDescription: %s\nVersion: %s",
|
||||
t.Errorf(
|
||||
"%s: got %v, want %v\nDescription: %s\nVersion: %s",
|
||||
tt.name,
|
||||
result,
|
||||
tt.expected,
|
||||
|
||||
+40
-51
@@ -7,10 +7,8 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -45,27 +43,27 @@ func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string, error) {
|
||||
// returning the raw flag id and the matched user.
|
||||
func resolveSingleUser(
|
||||
ctx context.Context,
|
||||
client v1.HeadscaleServiceClient,
|
||||
client *apiv1.Client,
|
||||
cmd *cobra.Command,
|
||||
) (uint64, *v1.User, error) {
|
||||
) (uint64, *apiv1.User, error) {
|
||||
id, username, err := usernameAndIDFromFlag(cmd)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
users, err := client.ListUsers(ctx, &v1.ListUsersRequest{
|
||||
Name: username,
|
||||
Id: id,
|
||||
resp, err := client.ListUsers(ctx, apiv1.ListUsersParams{
|
||||
Name: optString(username),
|
||||
ID: optUint64(id),
|
||||
})
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("listing users: %w", err)
|
||||
}
|
||||
|
||||
if len(users.GetUsers()) != 1 {
|
||||
if len(resp.Users) != 1 {
|
||||
return 0, nil, errMultipleUsersMatch
|
||||
}
|
||||
|
||||
return id, users.GetUsers()[0], nil
|
||||
return id, &resp.Users[0], nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -102,37 +100,32 @@ var createUserCmd = &cobra.Command{
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
userName := args[0]
|
||||
|
||||
log.Trace().Interface(zf.Client, client).Msg("obtained gRPC client")
|
||||
|
||||
request := &v1.CreateUserRequest{Name: userName}
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
req := &apiv1.CreateUserReq{Name: apiv1.NewOptString(args[0])}
|
||||
|
||||
if displayName, _ := cmd.Flags().GetString("display-name"); displayName != "" {
|
||||
request.DisplayName = displayName
|
||||
req.DisplayName = apiv1.NewOptString(displayName)
|
||||
}
|
||||
|
||||
if email, _ := cmd.Flags().GetString("email"); email != "" {
|
||||
request.Email = email
|
||||
req.Email = apiv1.NewOptString(email)
|
||||
}
|
||||
|
||||
if pictureURL, _ := cmd.Flags().GetString("picture-url"); pictureURL != "" {
|
||||
if _, err := url.Parse(pictureURL); err != nil { //nolint:noinlineerr
|
||||
_, err := url.Parse(pictureURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid picture URL: %w", err)
|
||||
}
|
||||
|
||||
request.PictureUrl = pictureURL
|
||||
req.PictureUrl = apiv1.NewOptString(pictureURL)
|
||||
}
|
||||
|
||||
log.Trace().Interface(zf.Request, request).Msg("sending CreateUser request")
|
||||
|
||||
response, err := client.CreateUser(ctx, request)
|
||||
resp, err := client.CreateUser(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating user: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetUser(), "User created")
|
||||
return printOutput(cmd, resp.User.Value, "User created")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -140,7 +133,7 @@ var destroyUserCmd = &cobra.Command{
|
||||
Use: "destroy --identifier ID or --name NAME",
|
||||
Short: "Destroys a user",
|
||||
Aliases: []string{cmdDelete},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
_, user, err := resolveSingleUser(ctx, client, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -148,19 +141,17 @@ var destroyUserCmd = &cobra.Command{
|
||||
|
||||
if !confirmAction(cmd, fmt.Sprintf(
|
||||
"Do you want to remove the user %q (%d) and any associated preauthkeys?",
|
||||
user.GetName(), user.GetId(),
|
||||
user.Name.Value, user.ID.Value,
|
||||
)) {
|
||||
return printOutput(cmd, map[string]string{colResult: "User not destroyed"}, "User not destroyed")
|
||||
}
|
||||
|
||||
deleteRequest := &v1.DeleteUserRequest{Id: user.GetId()}
|
||||
|
||||
response, err := client.DeleteUser(ctx, deleteRequest)
|
||||
err = client.DeleteUser(ctx, apiv1.DeleteUserParams{ID: user.ID.Value})
|
||||
if err != nil {
|
||||
return fmt.Errorf("destroying user: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "User destroyed")
|
||||
return printOutput(cmd, map[string]string{colResult: "User destroyed"}, "User destroyed")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -168,8 +159,8 @@ var listUsersCmd = &cobra.Command{
|
||||
Use: cmdList,
|
||||
Short: "List all the users",
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
request := &v1.ListUsersRequest{}
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
var params apiv1.ListUsersParams
|
||||
|
||||
id, _ := cmd.Flags().GetInt64("identifier")
|
||||
username, _ := cmd.Flags().GetString("name")
|
||||
@@ -178,29 +169,29 @@ var listUsersCmd = &cobra.Command{
|
||||
// filter by one param at most
|
||||
switch {
|
||||
case id > 0:
|
||||
request.Id = uint64(id)
|
||||
params.ID = apiv1.NewOptUint64(uint64(id))
|
||||
case username != "":
|
||||
request.Name = username
|
||||
params.Name = apiv1.NewOptString(username)
|
||||
case email != "":
|
||||
request.Email = email
|
||||
params.Email = apiv1.NewOptString(email)
|
||||
}
|
||||
|
||||
response, err := client.ListUsers(ctx, request)
|
||||
resp, err := client.ListUsers(ctx, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing users: %w", err)
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetUsers(), func() error {
|
||||
rows := make([][]string, 0, len(response.GetUsers()))
|
||||
for _, user := range response.GetUsers() {
|
||||
return printListOutput(cmd, resp.Users, func() error {
|
||||
rows := make([][]string, 0, len(resp.Users))
|
||||
for _, user := range resp.Users {
|
||||
rows = append(
|
||||
rows,
|
||||
[]string{
|
||||
strconv.FormatUint(user.GetId(), util.Base10),
|
||||
user.GetDisplayName(),
|
||||
user.GetName(),
|
||||
user.GetEmail(),
|
||||
user.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
|
||||
strconv.FormatUint(user.ID.Value, util.Base10),
|
||||
user.DisplayName.Value,
|
||||
user.Name.Value,
|
||||
user.Email.Value,
|
||||
user.CreatedAt.Value.Format(HeadscaleDateTimeFormat),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -214,7 +205,7 @@ var renameUserCmd = &cobra.Command{
|
||||
Use: "rename",
|
||||
Short: "Renames a user",
|
||||
Aliases: []string{"mv"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
|
||||
id, _, err := resolveSingleUser(ctx, client, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -222,16 +213,14 @@ var renameUserCmd = &cobra.Command{
|
||||
|
||||
newName, _ := cmd.Flags().GetString("new-name")
|
||||
|
||||
renameReq := &v1.RenameUserRequest{
|
||||
OldId: id,
|
||||
resp, err := client.RenameUser(ctx, apiv1.RenameUserParams{
|
||||
OldID: id,
|
||||
NewName: newName,
|
||||
}
|
||||
|
||||
response, err := client.RenameUser(ctx, renameReq)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("renaming user: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetUser(), "User renamed")
|
||||
return printOutput(cmd, resp.User.Value, "User renamed")
|
||||
}),
|
||||
}
|
||||
|
||||
+37
-171
@@ -1,28 +1,18 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/juanfont/headscale/hscontrol"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -69,144 +59,37 @@ func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) {
|
||||
return app, nil
|
||||
}
|
||||
|
||||
// grpcRunE wraps a cobra [cobra.Command.RunE] func, injecting a ready
|
||||
// gRPC client and context. Connection lifecycle is managed by the
|
||||
// wrapper — callers never see the underlying conn or cancel func.
|
||||
func grpcRunE(
|
||||
fn func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error,
|
||||
) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
// addressableForJSON makes value-typed generated API structs (and slice
|
||||
// elements) addressable so their pointer-receiver MarshalJSON is used. That
|
||||
// method omits unset optional fields; stdlib reflection over a value instead
|
||||
// calls Opt*.MarshalJSON directly, which returns empty for an unset field and
|
||||
// fails with "unexpected end of JSON input".
|
||||
func addressableForJSON(v any) any {
|
||||
rv := reflect.ValueOf(v)
|
||||
|
||||
return fn(ctx, client, cmd, args)
|
||||
}
|
||||
}
|
||||
|
||||
// withGRPC opens a gRPC client, runs fn with it, and tears the
|
||||
// connection down afterwards. It is the building block for commands
|
||||
// that branch on a flag before deciding to talk to the server, where
|
||||
// grpcRunE's whole-RunE wrapping does not fit.
|
||||
func withGRPC(
|
||||
fn func(ctx context.Context, client v1.HeadscaleServiceClient) error,
|
||||
) error {
|
||||
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
|
||||
return fn(ctx, client)
|
||||
}
|
||||
|
||||
func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc, error) {
|
||||
cfg, err := types.LoadCLIConfig()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("loading configuration: %w", err)
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Dur("timeout", cfg.CLI.Timeout).
|
||||
Msgf("Setting timeout")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.CLI.Timeout)
|
||||
|
||||
grpcOptions := []grpc.DialOption{
|
||||
grpc.WithBlock(), //nolint:staticcheck // SA1019: deprecated but supported in 1.x
|
||||
}
|
||||
|
||||
address := cfg.CLI.Address
|
||||
|
||||
// If the address is not set, we assume that we are on the server hosting [hscontrol].
|
||||
if address == "" {
|
||||
log.Debug().
|
||||
Str("socket", cfg.UnixSocket).
|
||||
Msgf("HEADSCALE_CLI_ADDRESS environment is not set, connecting to unix socket.")
|
||||
|
||||
address = cfg.UnixSocket
|
||||
|
||||
// Try to give the user better feedback if we cannot write to the headscale
|
||||
// socket. Note: [os.OpenFile] on a Unix domain socket returns ENXIO on
|
||||
// Linux which is expected — only permission errors are actionable here.
|
||||
// The actual gRPC connection uses [net.Dial] which handles sockets properly.
|
||||
socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) //nolint
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
cancel()
|
||||
|
||||
return nil, nil, nil, nil, fmt.Errorf(
|
||||
"unable to read/write to headscale socket %q, do you have the correct permissions? %w",
|
||||
cfg.UnixSocket,
|
||||
err,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
socket.Close()
|
||||
switch rv.Kind() { //nolint:exhaustive // default handles all other kinds
|
||||
case reflect.Slice, reflect.Array:
|
||||
out := make([]any, rv.Len())
|
||||
for i := range out {
|
||||
out[i] = addressableForJSON(rv.Index(i).Interface())
|
||||
}
|
||||
|
||||
grpcOptions = append(
|
||||
grpcOptions,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithContextDialer(util.GrpcSocketDialer),
|
||||
)
|
||||
} else {
|
||||
// If we are not connecting to a local server, require an API key for authentication
|
||||
apiKey := cfg.CLI.APIKey
|
||||
if apiKey == "" {
|
||||
cancel()
|
||||
return out
|
||||
case reflect.Struct:
|
||||
p := reflect.New(rv.Type())
|
||||
p.Elem().Set(rv)
|
||||
|
||||
return nil, nil, nil, nil, errAPIKeyNotSet
|
||||
}
|
||||
|
||||
grpcOptions = append(
|
||||
grpcOptions,
|
||||
grpc.WithPerRPCCredentials(tokenAuth{
|
||||
token: apiKey,
|
||||
}),
|
||||
)
|
||||
|
||||
if cfg.CLI.Insecure {
|
||||
tlsConfig := &tls.Config{
|
||||
// turn of gosec as we are intentionally setting
|
||||
// insecure.
|
||||
//nolint:gosec
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
grpcOptions = append(
|
||||
grpcOptions,
|
||||
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
|
||||
)
|
||||
} else {
|
||||
grpcOptions = append(
|
||||
grpcOptions,
|
||||
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
|
||||
)
|
||||
}
|
||||
return p.Interface()
|
||||
default:
|
||||
return v
|
||||
}
|
||||
|
||||
log.Trace().Caller().Str(zf.Address, address).Msg("connecting via gRPC")
|
||||
|
||||
conn, err := grpc.DialContext(ctx, address, grpcOptions...) //nolint:staticcheck // SA1019: deprecated but supported in 1.x
|
||||
if err != nil {
|
||||
cancel()
|
||||
|
||||
return nil, nil, nil, nil, fmt.Errorf("connecting to %s: %w", address, err)
|
||||
}
|
||||
|
||||
client := v1.NewHeadscaleServiceClient(conn)
|
||||
|
||||
return ctx, client, conn, cancel, nil
|
||||
}
|
||||
|
||||
// formatOutput serialises result into the requested format. For the
|
||||
// default (empty) format the human-readable override string is returned.
|
||||
func formatOutput(result any, override string, outputFormat string) (string, error) {
|
||||
result = addressableForJSON(result)
|
||||
|
||||
switch outputFormat {
|
||||
case outputFormatJSON:
|
||||
b, err := json.MarshalIndent(result, "", "\t")
|
||||
@@ -223,7 +106,21 @@ func formatOutput(result any, override string, outputFormat string) (string, err
|
||||
|
||||
return string(b), nil
|
||||
case outputFormatYAML:
|
||||
b, err := yaml.Marshal(result)
|
||||
// Route through JSON so types with a custom MarshalJSON (the generated
|
||||
// API types) serialise by their JSON shape, then convert to YAML.
|
||||
j, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshalling output: %w", err)
|
||||
}
|
||||
|
||||
var generic any
|
||||
|
||||
err = yaml.Unmarshal(j, &generic)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("converting output to YAML: %w", err)
|
||||
}
|
||||
|
||||
b, err := yaml.Marshal(generic)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshalling YAML output: %w", err)
|
||||
}
|
||||
@@ -249,19 +146,6 @@ func printOutput(cmd *cobra.Command, result any, override string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// expirationFromFlag parses the --expiration flag as a Prometheus-style
|
||||
// duration (e.g. "90d", "1h") and returns an absolute timestamp.
|
||||
func expirationFromFlag(cmd *cobra.Command) (*timestamppb.Timestamp, error) {
|
||||
durationStr, _ := cmd.Flags().GetString("expiration")
|
||||
|
||||
duration, err := model.ParseDuration(durationStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing duration: %w", err)
|
||||
}
|
||||
|
||||
return timestamppb.New(time.Now().UTC().Add(time.Duration(duration))), nil
|
||||
}
|
||||
|
||||
// confirmAction returns true when the user confirms a prompt, or when
|
||||
// --force is set. Callers decide what to do when it returns false.
|
||||
func confirmAction(cmd *cobra.Command, prompt string) bool {
|
||||
@@ -323,21 +207,3 @@ func hasMachineOutputFlag() bool {
|
||||
return arg == outputFormatJSON || arg == outputFormatJSONLine || arg == outputFormatYAML
|
||||
})
|
||||
}
|
||||
|
||||
type tokenAuth struct {
|
||||
token string
|
||||
}
|
||||
|
||||
// Return value is mapped to request headers.
|
||||
func (t tokenAuth) GetRequestMetadata(
|
||||
ctx context.Context,
|
||||
in ...string,
|
||||
) (map[string]string, error) {
|
||||
return map[string]string{
|
||||
"authorization": "Bearer " + t.token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tokenAuth) RequireTransportSecurity() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// sampleNode builds a fully-populated node with valid machine/node keys so the
|
||||
// table renderer's UnmarshalText/ParseAddr paths exercise real data.
|
||||
func sampleNode(t *testing.T) apiv1.Node {
|
||||
t.Helper()
|
||||
|
||||
mkey, err := key.NewMachine().Public().MarshalText()
|
||||
if err != nil {
|
||||
t.Fatalf("machine key: %v", err)
|
||||
}
|
||||
|
||||
nkey, err := key.NewNode().Public().MarshalText()
|
||||
if err != nil {
|
||||
t.Fatalf("node key: %v", err)
|
||||
}
|
||||
|
||||
return apiv1.Node{
|
||||
ID: apiv1.NewOptUint64(7),
|
||||
Name: apiv1.NewOptString("host7"),
|
||||
GivenName: apiv1.NewOptString("host7"),
|
||||
MachineKey: apiv1.NewOptString(string(mkey)),
|
||||
NodeKey: apiv1.NewOptString(string(nkey)),
|
||||
User: apiv1.NewOptUser(apiv1.User{Name: apiv1.NewOptString("alice")}),
|
||||
IpAddresses: []string{"100.64.0.7", "fd7a:115c:a1e0::7"},
|
||||
Online: apiv1.NewOptBool(true),
|
||||
// Expiry, LastSeen, Tags deliberately unset.
|
||||
}
|
||||
}
|
||||
|
||||
// TestFormatOutputValueWithUnsetOptional guards against a regression where the
|
||||
// generated API types, passed by value with unset optional fields, fail to
|
||||
// marshal because stdlib reflection calls Opt*.MarshalJSON directly.
|
||||
func TestFormatOutputValueWithUnsetOptional(t *testing.T) {
|
||||
cases := map[string]any{
|
||||
"user": apiv1.User{ID: apiv1.NewOptUint64(1), Name: apiv1.NewOptString("alice")},
|
||||
"node": sampleNode(t),
|
||||
"preauthkey": apiv1.PreAuthKey{ID: apiv1.NewOptUint64(2), Key: apiv1.NewOptString("abc")},
|
||||
"apikey": apiv1.ApiKey{ID: apiv1.NewOptUint64(3), Prefix: apiv1.NewOptString("pref")},
|
||||
}
|
||||
|
||||
formats := []string{outputFormatJSON, outputFormatJSONLine, outputFormatYAML}
|
||||
|
||||
for name, val := range cases {
|
||||
for _, format := range formats {
|
||||
// Single value.
|
||||
out, err := formatOutput(val, "", format)
|
||||
if err != nil {
|
||||
t.Fatalf("formatOutput(%s, %s) value: %v", name, format, err)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(out) == "" {
|
||||
t.Errorf("formatOutput(%s, %s) value: empty output", name, format)
|
||||
}
|
||||
|
||||
// Slice of values (the "list" commands).
|
||||
sliceOut, err := formatOutput([]any{val}, "", format)
|
||||
if err != nil {
|
||||
t.Fatalf("formatOutput(%s, %s) slice: %v", name, format, err)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(sliceOut) == "" {
|
||||
t.Errorf("formatOutput(%s, %s) slice: empty output", name, format)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFormatOutputJSONShape pins that a value-typed object serialises by its
|
||||
// JSON field names (not the Go struct shape) and omits unset optionals.
|
||||
func TestFormatOutputJSONShape(t *testing.T) {
|
||||
user := apiv1.User{ID: apiv1.NewOptUint64(1), Name: apiv1.NewOptString("alice")}
|
||||
|
||||
out, err := formatOutput(user, "", outputFormatJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("formatOutput: %v", err)
|
||||
}
|
||||
|
||||
var decoded map[string]any
|
||||
|
||||
err = json.Unmarshal([]byte(out), &decoded)
|
||||
if err != nil {
|
||||
t.Fatalf("output is not valid JSON: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
if decoded["name"] != "alice" || decoded["id"] != float64(1) {
|
||||
t.Errorf("unexpected JSON shape: %s", out)
|
||||
}
|
||||
|
||||
if _, present := decoded["displayName"]; present {
|
||||
t.Errorf("unset optional should be omitted, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNodesToPtables exercises the default table renderer, whose manual
|
||||
// MachineKey/NodeKey UnmarshalText and IP ParseAddr have no other coverage.
|
||||
func TestNodesToPtables(t *testing.T) {
|
||||
data, err := nodesToPtables([]apiv1.Node{sampleNode(t)})
|
||||
if err != nil {
|
||||
t.Fatalf("nodesToPtables: %v", err)
|
||||
}
|
||||
|
||||
if len(data) != 2 {
|
||||
t.Fatalf("want header + 1 row, got %d rows", len(data))
|
||||
}
|
||||
|
||||
row := strings.Join(data[1], "|")
|
||||
for _, want := range []string{"host7", "alice", "100.64.0.7", "fd7a:115c:a1e0::7"} {
|
||||
if !strings.Contains(row, want) {
|
||||
t.Errorf("row missing %q: %s", want, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNodesToPtablesEmptyNodeKey documents that a missing NodeKey makes the
|
||||
// table render fail loudly rather than silently — so a decode regression that
|
||||
// drops NodeKey over HTTP is caught at the CLI, not just in -o json.
|
||||
func TestNodesToPtablesEmptyNodeKey(t *testing.T) {
|
||||
node := sampleNode(t)
|
||||
node.NodeKey = apiv1.OptString{}
|
||||
|
||||
_, err := nodesToPtables([]apiv1.Node{node})
|
||||
if err == nil {
|
||||
t.Error("expected error for empty NodeKey, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNodeRoutesToPtables renders the list-routes table, which has zero
|
||||
// integration coverage.
|
||||
func TestNodeRoutesToPtables(t *testing.T) {
|
||||
node := sampleNode(t)
|
||||
node.SubnetRoutes = []string{"10.0.0.0/24"}
|
||||
node.ApprovedRoutes = []string{"10.0.0.0/24"}
|
||||
|
||||
data := nodeRoutesToPtables([]apiv1.Node{node})
|
||||
if len(data) != 2 {
|
||||
t.Fatalf("want header + 1 row, got %d rows", len(data))
|
||||
}
|
||||
|
||||
if !strings.Contains(strings.Join(data[1], "|"), "10.0.0.0/24") {
|
||||
t.Errorf("route row missing approved route: %v", data[1])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestUsernameAndIDFromFlag(t *testing.T) {
|
||||
newCmd := func() *cobra.Command {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringP("name", "n", "", "")
|
||||
cmd.Flags().Int64P("identifier", "i", -1, "")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
t.Run("neither flag is required", func(t *testing.T) {
|
||||
_, _, err := usernameAndIDFromFlag(newCmd())
|
||||
if !errors.Is(err, errFlagRequired) {
|
||||
t.Fatalf("want errFlagRequired, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("name only", func(t *testing.T) {
|
||||
cmd := newCmd()
|
||||
_ = cmd.Flags().Set("name", "alice")
|
||||
|
||||
id, name, err := usernameAndIDFromFlag(cmd)
|
||||
if err != nil || id != 0 || name != "alice" {
|
||||
t.Fatalf("got id=%d name=%q err=%v", id, name, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("id only", func(t *testing.T) {
|
||||
cmd := newCmd()
|
||||
_ = cmd.Flags().Set("identifier", "7")
|
||||
|
||||
id, name, err := usernameAndIDFromFlag(cmd)
|
||||
if err != nil || id != 7 || name != "" {
|
||||
t.Fatalf("got id=%d name=%q err=%v", id, name, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestApiKeyIDOrPrefix(t *testing.T) {
|
||||
newCmd := func() *cobra.Command {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Uint64P("id", "i", 0, "")
|
||||
cmd.Flags().StringP("prefix", "p", "", "")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
t.Run("neither is an error", func(t *testing.T) {
|
||||
_, _, err := apiKeyIDOrPrefix(newCmd())
|
||||
if !errors.Is(err, errMissingParameter) {
|
||||
t.Fatalf("want errMissingParameter, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("both is an error", func(t *testing.T) {
|
||||
cmd := newCmd()
|
||||
_ = cmd.Flags().Set("id", "1")
|
||||
_ = cmd.Flags().Set("prefix", "abc")
|
||||
|
||||
_, _, err := apiKeyIDOrPrefix(cmd)
|
||||
if !errors.Is(err, errMissingParameter) {
|
||||
t.Fatalf("want errMissingParameter, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("id only", func(t *testing.T) {
|
||||
cmd := newCmd()
|
||||
_ = cmd.Flags().Set("id", "5")
|
||||
|
||||
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
||||
if err != nil || id != 5 || prefix != "" {
|
||||
t.Fatalf("got id=%d prefix=%q err=%v", id, prefix, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpirationFromFlag(t *testing.T) {
|
||||
newCmd := func() *cobra.Command {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().StringP("expiration", "e", "", "")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
t.Run("empty is unset", func(t *testing.T) {
|
||||
exp, err := expirationFromFlag(newCmd())
|
||||
if err != nil || exp.Set {
|
||||
t.Fatalf("want unset, got set=%v err=%v", exp.Set, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid duration is an error", func(t *testing.T) {
|
||||
cmd := newCmd()
|
||||
_ = cmd.Flags().Set("expiration", "not-a-duration")
|
||||
|
||||
_, err := expirationFromFlag(cmd)
|
||||
if err == nil {
|
||||
t.Fatal("want error for invalid duration")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid duration is in the future", func(t *testing.T) {
|
||||
cmd := newCmd()
|
||||
_ = cmd.Flags().Set("expiration", "1h")
|
||||
|
||||
exp, err := expirationFromFlag(cmd)
|
||||
if err != nil || !exp.Set {
|
||||
t.Fatalf("want set, got set=%v err=%v", exp.Set, err)
|
||||
}
|
||||
|
||||
if !exp.Value.After(time.Now()) {
|
||||
t.Errorf("expiration not in the future: %v", exp.Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -23,22 +23,6 @@ listen_addr: 127.0.0.1:8080
|
||||
# Use an empty value to disable the metrics listener.
|
||||
metrics_listen_addr: 127.0.0.1:9090
|
||||
|
||||
# Address to listen for gRPC.
|
||||
# gRPC is used for controlling a headscale server
|
||||
# remotely with the CLI
|
||||
# Note: Remote access _only_ works if you have
|
||||
# valid certificates.
|
||||
#
|
||||
# For production:
|
||||
# grpc_listen_addr: 0.0.0.0:50443
|
||||
grpc_listen_addr: 127.0.0.1:50443
|
||||
|
||||
# Allow the gRPC admin interface to run in INSECURE
|
||||
# mode. This is not recommended as the traffic will
|
||||
# be unencrypted. Only enable if you know what you
|
||||
# are doing.
|
||||
grpc_allow_insecure: false
|
||||
|
||||
# CIDR(s) of reverse proxies (e.g. 127.0.0.1/32) whose
|
||||
# True-Client-IP, X-Real-IP and X-Forwarded-For headers should
|
||||
# be honoured. Empty (default) ignores those headers; setting
|
||||
|
||||
+3
-3
@@ -152,15 +152,15 @@ See also <https://tailscale.com/docs/concepts/device-visibility>.
|
||||
Headscale checks if the policy is valid during startup and refuses to start if it detects an error. The error message
|
||||
indicates which part of the policy is invalid. Follow these steps to fix your policy:
|
||||
|
||||
- Dump the policy to a file: `headscale policy get --bypass-grpc-and-access-database-directly > policy.json`
|
||||
- Dump the policy to a file: `headscale policy get --bypass-server-and-access-database-directly > policy.json`
|
||||
- Edit and fixup `policy.json`. Use the command `headscale policy check --file policy.json` to validate the policy.
|
||||
- Load the modified policy: `headscale policy set --bypass-grpc-and-access-database-directly --file policy.json`
|
||||
- Load the modified policy: `headscale policy set --bypass-server-and-access-database-directly --file policy.json`
|
||||
- Start Headscale as usual.
|
||||
|
||||
!!! warning "Full server configuration required"
|
||||
|
||||
The above commands to get/set the policy require a complete server configuration file including database settings. A
|
||||
minimal config to [control Headscale via remote CLI](../ref/api.md#grpc) is not sufficient. You may use
|
||||
minimal config to [control Headscale via remote CLI](../ref/api.md#remote-control) is not sufficient. You may use
|
||||
`headscale -c /path/to/config.yaml` to specify the path to an alternative configuration file.
|
||||
|
||||
## How can I migrate back to the recommended IP prefixes?
|
||||
|
||||
+13
-16
@@ -1,10 +1,10 @@
|
||||
# API
|
||||
|
||||
Headscale provides a [HTTP REST API](#rest-api) and a [gRPC interface](#grpc) which may be used to integrate a [web
|
||||
interface](integration/web-ui.md), [remote control Headscale](#setup-remote-control) or provide a base for custom
|
||||
Headscale provides a [HTTP REST API](#rest-api) which may be used to integrate a [web
|
||||
interface](integration/web-ui.md), [remote control Headscale](#remote-control) or provide a base for custom
|
||||
integration and tooling.
|
||||
|
||||
Both interfaces require a valid API key before use. To create an API key, log into your Headscale server and generate
|
||||
The API requires a valid API key before use. To create an API key, log into your Headscale server and generate
|
||||
one with the default expiration of 90 days:
|
||||
|
||||
```shell
|
||||
@@ -58,15 +58,14 @@ Headscale server at `/swagger` for details.
|
||||
https://headscale.example.com/api/v1/auth/register
|
||||
```
|
||||
|
||||
## gRPC
|
||||
## Remote control
|
||||
|
||||
The gRPC interface can be used to control a Headscale instance from a remote machine with the `headscale` binary.
|
||||
The `headscale` binary can control a Headscale instance from a remote machine over the HTTP API.
|
||||
|
||||
### Prerequisite
|
||||
|
||||
- A workstation to run `headscale` (any supported platform, e.g. Linux).
|
||||
- A Headscale server with gRPC enabled.
|
||||
- Connections to the gRPC port (default: `50443`) are allowed.
|
||||
- The Headscale server reachable over HTTP(S).
|
||||
- Remote access requires an encrypted connection via TLS.
|
||||
- An [API key](#api) to authenticate with the Headscale server.
|
||||
|
||||
@@ -88,19 +87,20 @@ The gRPC interface can be used to control a Headscale instance from a remote mac
|
||||
|
||||
```yaml title="config.yaml"
|
||||
cli:
|
||||
address: <HEADSCALE_ADDRESS>:<PORT>
|
||||
address: <HEADSCALE_URL>
|
||||
api_key: <API_KEY>
|
||||
```
|
||||
|
||||
=== "Environment variables"
|
||||
|
||||
```shell
|
||||
export HEADSCALE_CLI_ADDRESS="<HEADSCALE_ADDRESS>:<PORT>"
|
||||
export HEADSCALE_CLI_ADDRESS="<HEADSCALE_URL>"
|
||||
export HEADSCALE_CLI_API_KEY="<API_KEY>"
|
||||
```
|
||||
|
||||
This instructs the `headscale` binary to connect to a remote instance at `<HEADSCALE_ADDRESS>:<PORT>`, instead of
|
||||
connecting to the local instance.
|
||||
This instructs the `headscale` binary to connect to a remote instance at `<HEADSCALE_URL>` (e.g.
|
||||
`https://headscale.example.com`), instead of connecting to the local instance. A bare host without a scheme is
|
||||
assumed to be `https`.
|
||||
|
||||
1. Test the connection by listing all nodes:
|
||||
|
||||
@@ -113,15 +113,12 @@ The gRPC interface can be used to control a Headscale instance from a remote mac
|
||||
|
||||
### Behind a proxy
|
||||
|
||||
It's possible to run the gRPC remote endpoint behind a reverse proxy, like Nginx, and have it run on the _same_ port as Headscale.
|
||||
|
||||
While this is _not a supported_ feature, an example on how this can be set up on
|
||||
[NixOS is shown here](https://github.com/kradalby/dotfiles/blob/4489cdbb19cddfbfae82cd70448a38fde5a76711/machines/headscale.oracldn/headscale.nix#L61-L91).
|
||||
The remote CLI uses the same HTTP API as everything else, so it works through the reverse proxy already in front of
|
||||
Headscale with no extra setup.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- Make sure you have the _same_ Headscale version on your server and workstation.
|
||||
- Ensure that connections to the gRPC port are allowed.
|
||||
- Verify that your TLS certificate is valid and trusted.
|
||||
- If you don't have access to a trusted certificate (e.g. from Let's Encrypt), either:
|
||||
- Add your self-signed certificate to the trust store of your OS _or_
|
||||
|
||||
@@ -69,7 +69,6 @@ on inbound requests with sanitized values. Headscale picks the first valid IP ad
|
||||
- A reverse proxy adds another layer of complexity that needs to be able to handle the [Tailscale Control
|
||||
Protocol](#websocket) properly. Be sure to test your setup without a reverse proxy before raising an issue.
|
||||
- STUN (used along with the [embedded DERP server](../derp.md)) requires udp/3478 to be served publicly.
|
||||
- [gRPC](../api.md#grpc) (used to remote control Headscale) may not be proxied.
|
||||
|
||||
## Reverse proxy specific configuration
|
||||
|
||||
@@ -84,14 +83,12 @@ is [assumed](../../setup/requirements.md):
|
||||
- Service for Tailscale clients is served via HTTPS on port 443.
|
||||
- The reverse proxy redirects HTTP to HTTPS and is terminating TLS.
|
||||
- Both Headscale and the reverse proxy are running on the same host.
|
||||
- [Metrics](../debug.md#metrics-and-debug-endpoint) and [gRPC](../api.md#grpc) are not proxied, those are available via
|
||||
localhost.
|
||||
- [Metrics](../debug.md#metrics-and-debug-endpoint) are not proxied, those are available via localhost.
|
||||
|
||||
```yaml title="config.yaml" hl_lines="1"
|
||||
server_url: https://<SERVER_NAME>
|
||||
listen_addr: 127.0.0.1:8080
|
||||
metrics_listen_addr: 127.0.0.1:9090
|
||||
grpc_listen_addr: 127.0.0.1:50443
|
||||
trusted_proxies:
|
||||
- 127.0.0.1/32
|
||||
- ::1/128
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
|
||||
**It might be outdated and it might miss necessary steps**.
|
||||
|
||||
Headscale can be built from source using the latest version of [Go](https://golang.org) and [Buf](https://buf.build)
|
||||
(Protobuf generator). See the [Contributing section in the GitHub
|
||||
README](https://github.com/juanfont/headscale#contributing) for more information.
|
||||
Headscale can be built from source using the latest version of [Go](https://golang.org). See the [Contributing
|
||||
section in the GitHub README](https://github.com/juanfont/headscale#contributing) for more information.
|
||||
|
||||
## OpenBSD
|
||||
|
||||
|
||||
@@ -26,9 +26,6 @@ The ports in use vary with the intended scenario and enabled features. Some of t
|
||||
- udp/3478
|
||||
- Expose publicly: yes
|
||||
- STUN, required if the [embedded DERP server](../ref/derp.md) is enabled
|
||||
- tcp/50443
|
||||
- Expose publicly: yes
|
||||
- Only required if the gRPC interface is used to [remote-control Headscale](../ref/api.md#grpc).
|
||||
- tcp/9090
|
||||
- Expose publicly: no
|
||||
- [Metrics and debug endpoint](../ref/debug.md#metrics-and-debug-endpoint)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# v1 API: behaviour changes (gRPC/grpc-gateway → ogen OpenAPI 3.0)
|
||||
|
||||
This file records every deliberate deviation of the new ogen-based v1 HTTP API
|
||||
from the previous gRPC + grpc-gateway behaviour. Anything not listed here is
|
||||
preserved: HTTP verbs and path templates, JSON property names (camelCase,
|
||||
matching protojson), enum values (`SCREAMING_SNAKE`, e.g.
|
||||
`REGISTER_METHOD_CLI`), RFC3339 timestamp strings, response envelopes
|
||||
(`{"user": …}`, `{"nodes": […]}`, …), and the set of operations and their side
|
||||
effects on the state layer.
|
||||
|
||||
Each entry: **what changed**, **why**, **client impact**.
|
||||
|
||||
## Wire format
|
||||
|
||||
### Integer IDs are JSON numbers, not strings
|
||||
|
||||
**What:** `id`, `nodeId`/`node_id`, `oldId`, `user`, and every other integer
|
||||
identifier is now a JSON number (`"id": 42`). grpc-gateway emitted protojson's
|
||||
int64-as-string form (`"id": "42"`).
|
||||
|
||||
**Why:** native integers are the idiomatic OpenAPI 3.0 / ogen representation,
|
||||
type-safe end to end, and remove per-field string⇄int conversion. Headscale
|
||||
identifiers are small and well within the JS safe-integer range.
|
||||
|
||||
**Client impact:** HTTP clients that read IDs as strings must read them as
|
||||
numbers. `headscale … -o json/yaml` output changes accordingly (the CLI now
|
||||
runs on the generated client).
|
||||
|
||||
### Error responses are RFC 7807 problem documents
|
||||
|
||||
**What:** errors are returned as `application/problem+json` with a body of
|
||||
`{type?, title, status, detail, instance?}`. grpc-gateway returned an
|
||||
`rpcStatus` envelope `{code, message, details[]}` as `application/json`, where
|
||||
`code` was a gRPC status code and `details` was effectively always empty.
|
||||
|
||||
**Why:** RFC 7807 is the standard HTTP error shape; the gRPC `code`/`details`
|
||||
fields were gRPC implementation leakage with no value over the HTTP status line.
|
||||
|
||||
**Client impact:** clients that parsed `{code, message}` must read
|
||||
`{status, detail}` and the `application/problem+json` content type. The HTTP
|
||||
status code itself is unchanged for equivalent conditions (e.g. unknown user →
|
||||
404, invalid argument → 400, bad/My missing API key → 401).
|
||||
|
||||
## Behaviour
|
||||
|
||||
### Client errors return 4xx consistently
|
||||
|
||||
**What:** client mistakes now map to the appropriate 4xx status instead of 500.
|
||||
Missing resources are `404` (e.g. `RenameUser`, `DeleteUser`, `GetNode`,
|
||||
`DeleteNode`); invalid input is `400` (e.g. an unparseable route in
|
||||
`SetApprovedRoutes`, a malformed registration key in `RegisterNode`, an invalid
|
||||
tag in `SetTags`, an unconfirmed `BackfillNodeIPs`). Many of these gRPC handlers
|
||||
returned a plain Go error, which grpc-gateway rendered as `500`.
|
||||
|
||||
**Why:** a missing resource or bad input is a client error, not a server error;
|
||||
4xx is the correct, consistent status.
|
||||
|
||||
**Client impact:** clients that treated these as 500 should treat them as 400/404.
|
||||
|
||||
### Health on database failure
|
||||
|
||||
**What:** `GET /api/v1/health` returns `200 {"databaseConnectivity": true}` when
|
||||
the database is reachable and `500` (problem document) when the ping fails. The
|
||||
gRPC implementation returned the ping error, which grpc-gateway rendered as a
|
||||
500; the `databaseConnectivity:false` body was never observable on failure.
|
||||
|
||||
**Why:** preserves the observable contract (200 healthy, 500 unhealthy) under
|
||||
the new error shape.
|
||||
|
||||
**Client impact:** none beyond the problem-document error shape above.
|
||||
|
||||
## CLI
|
||||
|
||||
### Remote CLI connects to the HTTP API, not the gRPC port
|
||||
|
||||
**What:** with a configured `cli.address` (or `HEADSCALE_CLI_ADDRESS`), the CLI
|
||||
now speaks HTTP to the headscale API URL rather than gRPC to `grpc_listen_addr`.
|
||||
A bare `host:port` is assumed to be `https://host:port`. Locally (no address)
|
||||
the CLI talks HTTP over the existing unix socket, unchanged in spirit — no API
|
||||
key needed, filesystem permissions are the trust boundary.
|
||||
|
||||
**Why:** the gRPC service and its TCP listener are removed; the CLI runs on the
|
||||
generated HTTP client.
|
||||
|
||||
**Client impact:** point `cli.address` at the headscale HTTP server (the same
|
||||
URL `server_url` is reachable on) instead of the gRPC address. `cli.api_key`
|
||||
and `cli.insecure` are unchanged.
|
||||
|
||||
### `delete`/`expire` commands print a result message
|
||||
|
||||
**What:** commands whose API operation has no response body (user/node/key
|
||||
delete, key expire, auth approve/reject) print a small
|
||||
`{"result": "..."}`-style object (or the human-readable message) instead of the
|
||||
previous empty `{}`.
|
||||
|
||||
**Why:** the operations return no content; a result message is more useful than
|
||||
an empty object.
|
||||
|
||||
**Client impact:** scripts parsing the empty `{}` should read the `result`
|
||||
field (machine-readable output) or rely on the exit code.
|
||||
|
||||
### CLI database-bypass flag renamed
|
||||
|
||||
**What:** `--bypass-grpc-and-access-database-directly` is now
|
||||
`--bypass-server-and-access-database-directly`.
|
||||
|
||||
**Why:** the gRPC server is gone; the flag bypasses the running server whatever
|
||||
its transport, so the name no longer mentions gRPC.
|
||||
|
||||
**Client impact:** scripts using the old flag name must update it.
|
||||
|
||||
### Missing resources return a consistent `404`
|
||||
|
||||
**What:** renaming or expiring an unknown node, and expiring or deleting an
|
||||
unknown pre-auth key, now return `404 Not Found`. Previously the node
|
||||
operations surfaced as `500` and the pre-auth key operations reported success
|
||||
without changing anything.
|
||||
|
||||
**Why:** a missing resource is a client error, not a server error, and an
|
||||
expire or delete that matched no row should not report success.
|
||||
|
||||
**Client impact:** code that treated these as `500` or as a silent success
|
||||
should handle `404`.
|
||||
|
||||
## Delivery note (not a shipped behaviour change)
|
||||
|
||||
The grpc-gateway HTTP facade is replaced wholesale at `/api/v1` by the ogen
|
||||
server in the foundation commit, rather than path-by-path. During development,
|
||||
endpoints whose resource group has not yet been migrated return `501` over HTTP;
|
||||
all are implemented before the branch is complete. The CLI is unaffected during
|
||||
this window because it still uses the gRPC servers until its own migration. No
|
||||
intermediate state is released.
|
||||
@@ -0,0 +1,147 @@
|
||||
# Plan: Headscale v1 API — gRPC/Protobuf → ogen OpenAPI 3.0
|
||||
|
||||
This is the implementation plan for converting the Headscale v1 API from a
|
||||
Protobuf gRPC service with a grpc-gateway REST facade to an OpenAPI 3.0 spec
|
||||
driving ogen-generated server stubs and a Go client, backed unchanged by the
|
||||
`hscontrol/state` layer. It is written so coding agents can execute the
|
||||
remaining work autonomously; tests are the gate.
|
||||
|
||||
See [CHANGES.md](./CHANGES.md) for every deliberate behaviour deviation.
|
||||
|
||||
## Locked decisions
|
||||
|
||||
- **Integer IDs** are native JSON numbers (`type: integer, format: uint64`), not protojson strings.
|
||||
- **Errors** are RFC 7807 problem documents (`application/problem+json`).
|
||||
- **Local CLI** talks HTTP over the existing unix socket (no API key; socket permissions are the trust boundary). Remote stays HTTPS + bearer key.
|
||||
- Preserved by default: HTTP verbs/paths, camelCase JSON keys, `SCREAMING_SNAKE` enums, RFC3339 timestamps, response envelopes, operation set and state side effects.
|
||||
|
||||
## Package layout (v1 self-contained; v2 drops in as a sibling)
|
||||
|
||||
| Concern | Path | Package |
|
||||
| --------------------------- | --------------------------- | -------------------------------- |
|
||||
| Spec (source of truth) | `openapi/v1/headscale.yaml` | — |
|
||||
| Generated server+client | `gen/api/v1/` | `apiv1` (imported as `oas`) |
|
||||
| Handlers / state adapter | `hscontrol/api/v1/` | `apiv1` |
|
||||
| HTTP-parity tests + harness | `hscontrol/servertest/` | `servertest` / `servertest_test` |
|
||||
|
||||
v1 and v2 must not import each other or share generated types — they converge
|
||||
only on `hscontrol/state`. Codegen: `go generate ./gen/api/v1/` (also run by
|
||||
`make generate` via `go generate ./...`); CI freshness via
|
||||
`.github/workflows/check-generated.yml`. Never hand-edit generated code; fix the
|
||||
spec, never reach for `x-ogen` without recording in CHANGES.md why no spec
|
||||
change works.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Routing.** chi is the top mux. The ogen server (`http.Handler`) is mounted at `/api/v1/*` in `app.go` `createRouter`; chi leaves `r.URL.Path` intact so ogen's router matches the full path. ogen owns `/api/v1` wholesale: un-migrated operations inherit `oas.UnimplementedHandler` (501) until their unit lands. Parallel endpoint agents therefore touch only their handler file + tests — never `app.go`.
|
||||
- **Auth.** Spec declares `bearerAuth` (http/bearer) global security. `Server.HandleBearerAuth` → `state.ValidateAPIKey`; any failure → 401. (Unix-socket auth bypass is added in the CLI phase, when the socket serves HTTP.)
|
||||
- **Errors.** `Server.NewError` (ogen's convenient-error hook for security/decoding) and `errorHandler` (`WithErrorHandler`, for plain handler errors) both route through `classify` in `hscontrol/api/v1/errors.go`: typed `*oas.ErrorStatusCode` pass through; ogen framework errors use their `Code()` (security→401, decode→400); state errors via `mapStateError` (not-found sentinels→404, `ErrPolicyUpdateIsDisabled`→400, else 500). Handlers build expected errors with `apiError`/`notFound`/`badRequest`/`mapStateError`.
|
||||
- **Side effects.** Handlers replicate the exact `state.*` call sequence and `change(...)` notifications from `hscontrol/grpcv1.go` (policy reload on SetPolicy, route auto-approval on RegisterNode, change broadcasts on node mutations, etc.). The adapter holds `state *state.State`, `cfg *types.Config`, and `change func(...change.Change)` (wired to `Headscale.Change`); it does not import the parent `hscontrol` package (no import cycle).
|
||||
- **Transitional gRPC.** The gRPC unix-socket and TCP servers keep running through the endpoint phase so the existing CLI works; they (and proto/gateway code) are removed in cutover.
|
||||
|
||||
## Operation inventory (backbone) → state mapping
|
||||
|
||||
All paths keep the current `google.api.http` templates. Source of truth for
|
||||
behaviour: `hscontrol/grpcv1.go`.
|
||||
|
||||
**User** — `hscontrol/api/v1/users.go`
|
||||
|
||||
- `POST /api/v1/user` CreateUser → `state.CreateUser`; `change(policyChanged)`.
|
||||
- `GET /api/v1/user` ListUsers(`id`,`name`,`email` query) → `ListUsersWithFilter` (by name|email|id) else `ListAllUsers`; sort by id.
|
||||
- `POST /api/v1/user/{old_id}/rename/{new_name}` RenameUser → `GetUserByID`,`RenameUser`,`change(c)`,`GetUserByName`.
|
||||
- `DELETE /api/v1/user/{id}` DeleteUser → `GetUserByID`,`DeleteUser`,`change`.
|
||||
|
||||
**PreAuthKey** — `preauthkeys.go`
|
||||
|
||||
- `POST /api/v1/preauthkey` CreatePreAuthKey → validate each `aclTags` (`validateTag`), optional `GetUserByID`, `CreatePreAuthKey(userID, reusable, ephemeral, &expiration, aclTags)`.
|
||||
- `GET /api/v1/preauthkey` ListPreAuthKeys → `ListPreAuthKeys`; sort by id.
|
||||
- `POST /api/v1/preauthkey/expire` ExpirePreAuthKey → `ExpirePreAuthKey(id)`.
|
||||
- `DELETE /api/v1/preauthkey` DeletePreAuthKey(`id` query) → `DeletePreAuthKey(id)`.
|
||||
|
||||
**Node** — `nodes.go`
|
||||
|
||||
- `GET /api/v1/node` ListNodes(`user` query) → `GetUserByName`+`ListNodesByUser` | `ListNodes`; `nodesToProto` (tagged→`TaggedDevices` user; `subnetRoutes` = primary+exit routes); sort by id.
|
||||
- `POST /api/v1/node/register` RegisterNode(`user`,`key` query) → `AuthIDFromString`,`GetUserByName`,`HandleNodeFromAuthPath(...,RegisterMethodCLI)`,`AutoApproveRoutes`,`change(nodeChange,routeChange)`.
|
||||
- `POST /api/v1/node/backfillips` BackfillNodeIPs(`confirmed` query) → require confirmed else error; `BackfillNodeIPs`.
|
||||
- `GET /api/v1/node/{node_id}` GetNode → `GetNodeByID`; not found → 404.
|
||||
- `DELETE /api/v1/node/{node_id}` DeleteNode → `GetNodeByID`(404),`DeleteNode`,`change`.
|
||||
- `POST /api/v1/node/{node_id}/tags` SetTags → non-empty tags else 400; `validateTag` each; `GetNodeByID`(404); `SetNodeTags`(invalid→400); `change`.
|
||||
- `POST /api/v1/node/{node_id}/approve_routes` SetApprovedRoutes → parse prefixes; exit-route expansion (AllIPv4/AllIPv6 → add both); sort+compact; `SetApprovedRoutes`(invalid→400); `change`; set `subnetRoutes` = `GetNodePrimaryRoutes`.
|
||||
- `POST /api/v1/node/{node_id}/expire` ExpireNode(`expiry`,`disable_expiry` query) → both set → 400; disable → `SetNodeExpiry(nil)`; else `SetNodeExpiry(&expiry)` (default now); `change`.
|
||||
- `POST /api/v1/node/{node_id}/rename/{new_name}` RenameNode → `RenameNode`,`change`.
|
||||
- `POST /api/v1/debug/node` DebugCreateNode → `GetUserByName`,`StringToIPPrefix`,`AuthIDFromString`,`SetAuthCacheEntry`; echo synthetic node.
|
||||
|
||||
**Auth** — `auth.go`
|
||||
|
||||
- `POST /api/v1/auth/register` AuthRegister → delegates to RegisterNode(`key`=authId,`user`).
|
||||
- `POST /api/v1/auth/approve` AuthApprove → `AuthIDFromString`(400),`GetAuthCacheEntry`(404),`FinishAuth({})`.
|
||||
- `POST /api/v1/auth/reject` AuthReject → `AuthIDFromString`(400),`GetAuthCacheEntry`(404),`FinishAuth({Err})`.
|
||||
|
||||
**ApiKey** — `apikeys.go`
|
||||
|
||||
- `POST /api/v1/apikey` CreateApiKey → `CreateAPIKey(&expiration)`; returns raw `apiKey` string.
|
||||
- `GET /api/v1/apikey` ListApiKeys → `ListAPIKeys`; sort by id.
|
||||
- `POST /api/v1/apikey/expire` ExpireApiKey → `getAPIKey` (id xor prefix; neither/both→400),`ExpireAPIKey`.
|
||||
- `DELETE /api/v1/apikey/{prefix}` DeleteApiKey(`id` query) → `getAPIKey`,`DestroyAPIKey`.
|
||||
|
||||
**Policy** — `policy.go`
|
||||
|
||||
- `GET /api/v1/policy` GetPolicy → DB mode: `GetPolicy`; file mode: read `cfg.Policy.Path`.
|
||||
- `PUT /api/v1/policy` SetPolicy → DB mode only (`ErrPolicyUpdateIsDisabled`→400); `ListNodes`,`SetPolicy`,`SSHPolicy(node0)`,`SetPolicyInDB`,`ReloadPolicy`,`change(cs...)`.
|
||||
- `POST /api/v1/policy/check` CheckPolicy → `ListAllUsers`,`ListNodes`,`NewPolicyManager`+`SetPolicy` (no persist; invalid→400).
|
||||
|
||||
**Health** — `health.go` (done) — `PingDB`; ok→200, fail→500.
|
||||
|
||||
## Handler authoring recipe (for endpoint agents)
|
||||
|
||||
For each resource group, in this order, then `/commit` (Go style: `api: <imperative>`):
|
||||
|
||||
1. **Tests first.** Add `hscontrol/servertest/apiv1_<resource>_test.go` (package `servertest_test`). Use `srv := servertest.NewServer(t)`, `client := srv.APIClient(t, srv.CreateAPIKey(t))`, seed state via `srv.State()` / existing `servertest` helpers. Assert: success payloads (status, envelope, field values), every error path (404/400/401 with `*apiv1.ErrorStatusCode` status), and state side effects (re-read via `srv.State()`). Cover real behaviour, not just happy paths. Where the new contract intentionally differs from the old, the assertion encodes the new value **and** there is a matching CHANGES.md entry.
|
||||
1. **Implement** the group's handler methods on `*apiv1.Server` in `hscontrol/api/v1/<resource>.go`, mirroring the `grpcv1.go` logic and side effects exactly. Convert between ogen types (`*XxxReq`, `XxxParams`, `*XxxOK`, `oas.User`/`oas.Node`/…) and state types. Build `oas.OptXxx` for optional fields; map state results to the response envelope. Errors: `notFound`/`badRequest`/`mapStateError`/`apiError`.
|
||||
1. **CLI tests** for the group's commands belong to the CLI phase (`integration/`); reference them in the unit's acceptance.
|
||||
1. **CHANGES.md**: add an entry for any deviation; if a test had to encode a non-preserved behaviour, it must correspond to an entry.
|
||||
1. Run `goimports -w`, `go test ./hscontrol/servertest/ -run TestAPIv1_<Resource>`, then `make fmt`/`make lint` on touched files. Commit.
|
||||
|
||||
A reusable proto→ogen conversion (`oas.User` from `types.User`, etc.) should live in `hscontrol/api/v1/convert.go`; the first agent to need a converter adds it there and others reuse it. Do not duplicate.
|
||||
|
||||
## Work units & dependency map
|
||||
|
||||
```
|
||||
F1 spec → F2 codegen+CI → F3 server/adapter/auth/errors → F4 harness [DONE]
|
||||
│
|
||||
┌────────────────────────────────────────────────────┐
|
||||
E1 User E2 PreAuthKey E3 Node E4 Auth E5 ApiKey E6 Policy E7 Health(done)
|
||||
└────────────────────────────────────────────────────┘ (parallel; touch only own handler+tests)
|
||||
F2 → C0 CLI client adapter → C1..C8 per-command (each needs its E-unit) (parallel)
|
||||
all E* + all C* → X1 flip/auth → X2 remove proto/gRPC/gateway → X3 docs+served spec → X4 reconcile
|
||||
```
|
||||
|
||||
- **E1–E7**: each owns its handler file + `apiv1_<resource>_test.go`. Acceptance: that group's HTTP-parity tests green; behaviour matches `grpcv1.go` modulo CHANGES.md.
|
||||
- **C0**: rework `cmd/headscale/cli/utils.go` dial point to build `apiv1.Client` (local HTTP-over-unix, no key; remote HTTPS+`HEADSCALE_CLI_API_KEY`+`cli.insecure`). Serve ogen over the unix socket; add socket auth-bypass. **C1–C8**: convert each `cmd/headscale/cli/*.go` off the gRPC client; gate with `integration/` CLI tests. Acceptance: no `v1.HeadscaleServiceClient` usage remains.
|
||||
- **X1**: ogen is already the sole `/api/v1` handler; ensure no chi auth duplication, remove any dead gateway wiring. **X2**: delete `proto/headscale/v1`, `gen/go`, `gen/openapiv2`, `hscontrol/grpcv1.go`(+test), gRPC servers, `buf.*`; drop grpc/grpc-gateway deps (verify Noise is independent). **X3**: serve `openapi/v1/headscale.yaml` at `/swagger`; update `swagger.go` embed + `docs/`. **X4**: reconcile PLAN.md/CHANGES.md to the built state; DoD checklist green.
|
||||
|
||||
## Status
|
||||
|
||||
- [x] **F1** spec — `openapi/v1/headscale.yaml` (all 27 ops, native int64, RFC7807 `Problem`, bearerAuth). ogen generates cleanly.
|
||||
- [x] **F2** codegen — `gen/api/v1/generate.go` (`go generate`), ogen tool dep in `go.mod`, CI freshness covers `openapi/**`.
|
||||
- [x] **F3** server/adapter/auth/errors — `hscontrol/api/v1/{server,errors,auth,health}.go`; mounted at `/api/v1`; gateway unmounted; gRPC servers retained.
|
||||
- [x] **F4** harness — `servertest.APIClient`/`CreateAPIKey`; Health parity + 401 tests green.
|
||||
- [x] **E1–E7** endpoints — all 27 operations implemented in `hscontrol/api/v1/{users,apikeys,preauthkeys,nodes,auth,policy,health}.go` with HTTP-parity tests in `hscontrol/servertest/apiv1_*_test.go` (30 tests, all green).
|
||||
- [x] **C0–C8** CLI migration — server serves the API over the unix socket (auth bypassed via `apiv1.WithSocketAuth`); every `cmd/headscale/cli/*.go` command runs on the generated `apiv1.Client`; no gRPC client usage remains.
|
||||
- [x] **X1/X2** proto/gRPC removal — `grpcv1.go`, the TCP gRPC server + interceptor, the `.Proto()`/`RegisterMethodToV1Enum` builders, `proto/`, `gen/go/`, `gen/openapiv2/`, and `buf.gen.yaml` are deleted; `convert.go` reads the state views directly; `integration/` decodes into `apiv1` types. `grep` finds zero proto references in any `.go` file. grpc/grpc-gateway are no longer direct go.mod deps. Proto lint/format targets removed from the Makefile.
|
||||
- [x] **X3** served spec — `/swagger` serves the embedded `openapi/v1/headscale.yaml` (3.0).
|
||||
- [x] **X4** reconcile — this document and CHANGES.md reflect the built state.
|
||||
|
||||
The conversion is complete. The full `go test ./...` suite is validatable here for
|
||||
everything except the Docker-based `integration/` package, which is
|
||||
compile-clean (`go vet ./integration/...`) and ported to the new wire format; its
|
||||
end-to-end run is gated on Docker (`go run ./cmd/hi`).
|
||||
|
||||
## Definition of done
|
||||
|
||||
Every v1 op served by ogen over the state layer; HTTP + CLI tests cover every
|
||||
endpoint and pass; CLI on the generated client (no gRPC client); proto/gRPC +
|
||||
gateway + generated code removed; generated code matches committed spec
|
||||
(CI green); docs + `/swagger` point at the 3.0 spec; v1 self-contained, no
|
||||
cross-version coupling; current behaviour preserved except deliberate,
|
||||
test-matched, CHANGES.md-recorded deviations.
|
||||
@@ -66,40 +66,6 @@
|
||||
subPackages = [ "cmd/hi" ];
|
||||
};
|
||||
|
||||
protoc-gen-grpc-gateway = buildGo rec {
|
||||
pname = "grpc-gateway";
|
||||
version = "2.29.0";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "grpc-ecosystem";
|
||||
repo = "grpc-gateway";
|
||||
rev = "v${version}";
|
||||
sha256 = "sha256-d9OIIGttyMBSNgpS6mbR5JEIm13qGu2gFHJazJAexdw=";
|
||||
};
|
||||
|
||||
vendorHash = "sha256-p51yD+v8+rPs+ztlX7r0VQ4XlwUkxu+PxgknKEvH00k=";
|
||||
|
||||
nativeBuildInputs = [ pkgs.installShellFiles ];
|
||||
|
||||
subPackages = [ "protoc-gen-grpc-gateway" "protoc-gen-openapiv2" ];
|
||||
};
|
||||
|
||||
protobuf-language-server = buildGo rec {
|
||||
pname = "protobuf-language-server";
|
||||
version = "ab4c128";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "lasorda";
|
||||
repo = "protobuf-language-server";
|
||||
rev = "ab4c128f00774d51bd6d1f4cfa735f4b7c8619e3";
|
||||
sha256 = "sha256-yF6kG+qTRxVO/qp2V9HgTyFBeOm5RQzeqdZFrdidwxM=";
|
||||
};
|
||||
|
||||
vendorHash = "sha256-4nTpKBe7ekJsfQf+P6edT/9Vp2SBYbKz1ITawD3bhkI=";
|
||||
|
||||
subPackages = [ "." ];
|
||||
};
|
||||
|
||||
# Build golangci-lint with stock Go 1.26 (upstream uses hardcoded Go
|
||||
# version); it does not build against the pinned 1.26.4.
|
||||
golangci-lint = buildGo rec {
|
||||
@@ -195,15 +161,6 @@
|
||||
# 'dot' is needed for pprof graphs
|
||||
# go tool pprof -http=: <source>
|
||||
graphviz
|
||||
|
||||
# Protobuf dependencies
|
||||
protobuf
|
||||
protoc-gen-go
|
||||
protoc-gen-go-grpc
|
||||
protoc-gen-grpc-gateway
|
||||
buf
|
||||
clang-tools # clang-format
|
||||
protobuf-language-server
|
||||
]
|
||||
++ lib.optionals pkgs.stdenv.isLinux [ traceroute ];
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"vendor": {
|
||||
"goModSum": "sha256-csVm5v6HZ49PBp/FCX+yK1sjV8/nuUQz3GKN21Ne1mg=",
|
||||
"sri": "sha256-fzKyXNMw/2yAEhaTZu0n1NXatPO2IP0HFA2ey1vZIYM="
|
||||
"goModSum": "sha256-pE9YS5dCh93GWjRWlbDTmPLhvXbHIoHEKU/6qiN2ex4=",
|
||||
"sri": "sha256-KRgUvdDEVfkkwcEU6mxGWyCGXn/IMw9ZBVQmVGQZUcY="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Package apiv1 contains the ogen-generated server and client for the
|
||||
// Headscale v1 HTTP API. The code is generated from the committed OpenAPI 3.0
|
||||
// spec at openapi/v1/headscale.yaml — the single source of truth. Do not edit
|
||||
// generated files by hand; run `make generate` (or `go generate ./...`) after
|
||||
// changing the spec.
|
||||
//
|
||||
//go:generate go run github.com/ogen-go/ogen/cmd/ogen --target . --package apiv1 --clean ../../../openapi/v1/headscale.yaml
|
||||
package apiv1
|
||||
@@ -0,0 +1,318 @@
|
||||
// Code generated by ogen, DO NOT EDIT.
|
||||
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
ht "github.com/ogen-go/ogen/http"
|
||||
"github.com/ogen-go/ogen/middleware"
|
||||
"github.com/ogen-go/ogen/ogenerrors"
|
||||
"github.com/ogen-go/ogen/otelogen"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
// Allocate option closure once.
|
||||
clientSpanKind = trace.WithSpanKind(trace.SpanKindClient)
|
||||
// Allocate option closure once.
|
||||
serverSpanKind = trace.WithSpanKind(trace.SpanKindServer)
|
||||
)
|
||||
|
||||
type (
|
||||
optionFunc[C any] func(*C)
|
||||
otelOptionFunc func(*otelConfig)
|
||||
)
|
||||
|
||||
type otelConfig struct {
|
||||
TracerProvider trace.TracerProvider
|
||||
Tracer trace.Tracer
|
||||
MeterProvider metric.MeterProvider
|
||||
Meter metric.Meter
|
||||
Attributes []attribute.KeyValue
|
||||
}
|
||||
|
||||
func (cfg *otelConfig) initOTEL() {
|
||||
if cfg.TracerProvider == nil {
|
||||
cfg.TracerProvider = otel.GetTracerProvider()
|
||||
}
|
||||
if cfg.MeterProvider == nil {
|
||||
cfg.MeterProvider = otel.GetMeterProvider()
|
||||
}
|
||||
cfg.Tracer = cfg.TracerProvider.Tracer(otelogen.Name,
|
||||
trace.WithInstrumentationVersion(otelogen.SemVersion()),
|
||||
)
|
||||
cfg.Meter = cfg.MeterProvider.Meter(otelogen.Name,
|
||||
metric.WithInstrumentationVersion(otelogen.SemVersion()),
|
||||
)
|
||||
}
|
||||
|
||||
// ErrorHandler is error handler.
|
||||
type ErrorHandler = ogenerrors.ErrorHandler
|
||||
|
||||
type serverConfig struct {
|
||||
otelConfig
|
||||
NotFound http.HandlerFunc
|
||||
MethodNotAllowed func(w http.ResponseWriter, r *http.Request, allowed string)
|
||||
ErrorHandler ErrorHandler
|
||||
Prefix string
|
||||
Middleware Middleware
|
||||
MaxMultipartMemory int64
|
||||
}
|
||||
|
||||
// ServerOption is server config option.
|
||||
type ServerOption interface {
|
||||
applyServer(*serverConfig)
|
||||
}
|
||||
|
||||
var _ ServerOption = (optionFunc[serverConfig])(nil)
|
||||
|
||||
func (o optionFunc[C]) applyServer(c *C) {
|
||||
o(c)
|
||||
}
|
||||
|
||||
var _ ServerOption = (otelOptionFunc)(nil)
|
||||
|
||||
func (o otelOptionFunc) applyServer(c *serverConfig) {
|
||||
o(&c.otelConfig)
|
||||
}
|
||||
|
||||
func newServerConfig(opts ...ServerOption) serverConfig {
|
||||
cfg := serverConfig{
|
||||
NotFound: http.NotFound,
|
||||
MethodNotAllowed: nil,
|
||||
ErrorHandler: ogenerrors.DefaultErrorHandler,
|
||||
Middleware: nil,
|
||||
MaxMultipartMemory: 32 << 20, // 32 MB
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.applyServer(&cfg)
|
||||
}
|
||||
cfg.initOTEL()
|
||||
return cfg
|
||||
}
|
||||
|
||||
type baseServer struct {
|
||||
cfg serverConfig
|
||||
requests metric.Int64Counter
|
||||
errors metric.Int64Counter
|
||||
duration metric.Float64Histogram
|
||||
}
|
||||
|
||||
func (s baseServer) notFound(w http.ResponseWriter, r *http.Request) {
|
||||
s.cfg.NotFound(w, r)
|
||||
}
|
||||
|
||||
type notAllowedParams struct {
|
||||
allowedMethods string
|
||||
allowedHeaders map[string]string
|
||||
acceptPost string
|
||||
acceptPatch string
|
||||
}
|
||||
|
||||
func (s baseServer) notAllowed(w http.ResponseWriter, r *http.Request, params notAllowedParams) {
|
||||
h := w.Header()
|
||||
isOptions := r.Method == "OPTIONS"
|
||||
if isOptions {
|
||||
h.Set("Access-Control-Allow-Methods", params.allowedMethods)
|
||||
if params.allowedHeaders != nil {
|
||||
m := r.Header.Get("Access-Control-Request-Method")
|
||||
if m != "" {
|
||||
allowedHeaders, ok := params.allowedHeaders[strings.ToUpper(m)]
|
||||
if ok {
|
||||
h.Set("Access-Control-Allow-Headers", allowedHeaders)
|
||||
}
|
||||
}
|
||||
}
|
||||
if params.acceptPost != "" {
|
||||
h.Set("Accept-Post", params.acceptPost)
|
||||
}
|
||||
if params.acceptPatch != "" {
|
||||
h.Set("Accept-Patch", params.acceptPatch)
|
||||
}
|
||||
}
|
||||
if s.cfg.MethodNotAllowed != nil {
|
||||
s.cfg.MethodNotAllowed(w, r, params.allowedMethods)
|
||||
return
|
||||
}
|
||||
status := http.StatusNoContent
|
||||
if !isOptions {
|
||||
h.Set("Allow", params.allowedMethods)
|
||||
status = http.StatusMethodNotAllowed
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (cfg serverConfig) baseServer() (s baseServer, err error) {
|
||||
s = baseServer{cfg: cfg}
|
||||
if s.requests, err = otelogen.ServerRequestCountCounter(s.cfg.Meter); err != nil {
|
||||
return s, err
|
||||
}
|
||||
if s.errors, err = otelogen.ServerErrorsCountCounter(s.cfg.Meter); err != nil {
|
||||
return s, err
|
||||
}
|
||||
if s.duration, err = otelogen.ServerDurationHistogram(s.cfg.Meter); err != nil {
|
||||
return s, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type clientConfig struct {
|
||||
otelConfig
|
||||
Client ht.Client
|
||||
}
|
||||
|
||||
// ClientOption is client config option.
|
||||
type ClientOption interface {
|
||||
applyClient(*clientConfig)
|
||||
}
|
||||
|
||||
var _ ClientOption = (optionFunc[clientConfig])(nil)
|
||||
|
||||
func (o optionFunc[C]) applyClient(c *C) {
|
||||
o(c)
|
||||
}
|
||||
|
||||
var _ ClientOption = (otelOptionFunc)(nil)
|
||||
|
||||
func (o otelOptionFunc) applyClient(c *clientConfig) {
|
||||
o(&c.otelConfig)
|
||||
}
|
||||
|
||||
func newClientConfig(opts ...ClientOption) clientConfig {
|
||||
cfg := clientConfig{
|
||||
Client: http.DefaultClient,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.applyClient(&cfg)
|
||||
}
|
||||
cfg.initOTEL()
|
||||
return cfg
|
||||
}
|
||||
|
||||
type baseClient struct {
|
||||
cfg clientConfig
|
||||
requests metric.Int64Counter
|
||||
errors metric.Int64Counter
|
||||
duration metric.Float64Histogram
|
||||
}
|
||||
|
||||
func (cfg clientConfig) baseClient() (c baseClient, err error) {
|
||||
c = baseClient{cfg: cfg}
|
||||
if c.requests, err = otelogen.ClientRequestCountCounter(c.cfg.Meter); err != nil {
|
||||
return c, err
|
||||
}
|
||||
if c.errors, err = otelogen.ClientErrorsCountCounter(c.cfg.Meter); err != nil {
|
||||
return c, err
|
||||
}
|
||||
if c.duration, err = otelogen.ClientDurationHistogram(c.cfg.Meter); err != nil {
|
||||
return c, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Option is config option.
|
||||
type Option interface {
|
||||
ServerOption
|
||||
ClientOption
|
||||
}
|
||||
|
||||
// WithTracerProvider specifies a tracer provider to use for creating a tracer.
|
||||
//
|
||||
// If none is specified, the global provider is used.
|
||||
func WithTracerProvider(provider trace.TracerProvider) Option {
|
||||
return otelOptionFunc(func(cfg *otelConfig) {
|
||||
if provider != nil {
|
||||
cfg.TracerProvider = provider
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WithMeterProvider specifies a meter provider to use for creating a meter.
|
||||
//
|
||||
// If none is specified, the otel.GetMeterProvider() is used.
|
||||
func WithMeterProvider(provider metric.MeterProvider) Option {
|
||||
return otelOptionFunc(func(cfg *otelConfig) {
|
||||
if provider != nil {
|
||||
cfg.MeterProvider = provider
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WithAttributes specifies default otel attributes.
|
||||
func WithAttributes(attributes ...attribute.KeyValue) Option {
|
||||
return otelOptionFunc(func(cfg *otelConfig) {
|
||||
cfg.Attributes = attributes
|
||||
})
|
||||
}
|
||||
|
||||
// WithClient specifies http client to use.
|
||||
func WithClient(client ht.Client) ClientOption {
|
||||
return optionFunc[clientConfig](func(cfg *clientConfig) {
|
||||
if client != nil {
|
||||
cfg.Client = client
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WithNotFound specifies Not Found handler to use.
|
||||
func WithNotFound(notFound http.HandlerFunc) ServerOption {
|
||||
return optionFunc[serverConfig](func(cfg *serverConfig) {
|
||||
if notFound != nil {
|
||||
cfg.NotFound = notFound
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WithMethodNotAllowed specifies Method Not Allowed handler to use.
|
||||
func WithMethodNotAllowed(methodNotAllowed func(w http.ResponseWriter, r *http.Request, allowed string)) ServerOption {
|
||||
return optionFunc[serverConfig](func(cfg *serverConfig) {
|
||||
if methodNotAllowed != nil {
|
||||
cfg.MethodNotAllowed = methodNotAllowed
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WithErrorHandler specifies error handler to use.
|
||||
func WithErrorHandler(h ErrorHandler) ServerOption {
|
||||
return optionFunc[serverConfig](func(cfg *serverConfig) {
|
||||
if h != nil {
|
||||
cfg.ErrorHandler = h
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WithPathPrefix specifies server path prefix.
|
||||
func WithPathPrefix(prefix string) ServerOption {
|
||||
return optionFunc[serverConfig](func(cfg *serverConfig) {
|
||||
cfg.Prefix = prefix
|
||||
})
|
||||
}
|
||||
|
||||
// WithMiddleware specifies middlewares to use.
|
||||
func WithMiddleware(m ...Middleware) ServerOption {
|
||||
return optionFunc[serverConfig](func(cfg *serverConfig) {
|
||||
switch len(m) {
|
||||
case 0:
|
||||
cfg.Middleware = nil
|
||||
case 1:
|
||||
cfg.Middleware = m[0]
|
||||
default:
|
||||
cfg.Middleware = middleware.ChainMiddlewares(m...)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WithMaxMultipartMemory specifies limit of memory for storing file parts.
|
||||
// File parts which can't be stored in memory will be stored on disk in temporary files.
|
||||
func WithMaxMultipartMemory(max int64) ServerOption {
|
||||
return optionFunc[serverConfig](func(cfg *serverConfig) {
|
||||
if max > 0 {
|
||||
cfg.MaxMultipartMemory = max
|
||||
}
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
// Code generated by ogen, DO NOT EDIT.
|
||||
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
// Labeler is used to allow adding custom attributes to the server request metrics.
|
||||
type Labeler struct {
|
||||
attrs []attribute.KeyValue
|
||||
}
|
||||
|
||||
// Add attributes to the Labeler.
|
||||
func (l *Labeler) Add(attrs ...attribute.KeyValue) {
|
||||
l.attrs = append(l.attrs, attrs...)
|
||||
}
|
||||
|
||||
// AttributeSet returns the attributes added to the Labeler as an attribute.Set.
|
||||
func (l *Labeler) AttributeSet() attribute.Set {
|
||||
return attribute.NewSet(l.attrs...)
|
||||
}
|
||||
|
||||
type labelerContextKey struct{}
|
||||
|
||||
// LabelerFromContext retrieves the Labeler from the provided context, if present.
|
||||
//
|
||||
// If no Labeler was found in the provided context a new, empty Labeler is returned and the second
|
||||
// return value is false. In this case it is safe to use the Labeler but any attributes added to
|
||||
// it will not be used.
|
||||
func LabelerFromContext(ctx context.Context) (*Labeler, bool) {
|
||||
if l, ok := ctx.Value(labelerContextKey{}).(*Labeler); ok {
|
||||
return l, true
|
||||
}
|
||||
return &Labeler{}, false
|
||||
}
|
||||
|
||||
func contextWithLabeler(ctx context.Context, l *Labeler) context.Context {
|
||||
return context.WithValue(ctx, labelerContextKey{}, l)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Code generated by ogen, DO NOT EDIT.
|
||||
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"github.com/ogen-go/ogen/middleware"
|
||||
)
|
||||
|
||||
// Middleware is middleware type.
|
||||
type Middleware = middleware.Middleware
|
||||
@@ -0,0 +1,38 @@
|
||||
// Code generated by ogen, DO NOT EDIT.
|
||||
|
||||
package apiv1
|
||||
|
||||
// OperationName is the ogen operation name
|
||||
type OperationName = string
|
||||
|
||||
const (
|
||||
AuthApproveOperation OperationName = "AuthApprove"
|
||||
AuthRegisterOperation OperationName = "AuthRegister"
|
||||
AuthRejectOperation OperationName = "AuthReject"
|
||||
BackfillNodeIPsOperation OperationName = "BackfillNodeIPs"
|
||||
CheckPolicyOperation OperationName = "CheckPolicy"
|
||||
CreateApiKeyOperation OperationName = "CreateApiKey"
|
||||
CreatePreAuthKeyOperation OperationName = "CreatePreAuthKey"
|
||||
CreateUserOperation OperationName = "CreateUser"
|
||||
DebugCreateNodeOperation OperationName = "DebugCreateNode"
|
||||
DeleteApiKeyOperation OperationName = "DeleteApiKey"
|
||||
DeleteNodeOperation OperationName = "DeleteNode"
|
||||
DeletePreAuthKeyOperation OperationName = "DeletePreAuthKey"
|
||||
DeleteUserOperation OperationName = "DeleteUser"
|
||||
ExpireApiKeyOperation OperationName = "ExpireApiKey"
|
||||
ExpireNodeOperation OperationName = "ExpireNode"
|
||||
ExpirePreAuthKeyOperation OperationName = "ExpirePreAuthKey"
|
||||
GetNodeOperation OperationName = "GetNode"
|
||||
GetPolicyOperation OperationName = "GetPolicy"
|
||||
HealthOperation OperationName = "Health"
|
||||
ListApiKeysOperation OperationName = "ListApiKeys"
|
||||
ListNodesOperation OperationName = "ListNodes"
|
||||
ListPreAuthKeysOperation OperationName = "ListPreAuthKeys"
|
||||
ListUsersOperation OperationName = "ListUsers"
|
||||
RegisterNodeOperation OperationName = "RegisterNode"
|
||||
RenameNodeOperation OperationName = "RenameNode"
|
||||
RenameUserOperation OperationName = "RenameUser"
|
||||
SetApprovedRoutesOperation OperationName = "SetApprovedRoutes"
|
||||
SetPolicyOperation OperationName = "SetPolicy"
|
||||
SetTagsOperation OperationName = "SetTags"
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,938 @@
|
||||
// Code generated by ogen, DO NOT EDIT.
|
||||
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
"github.com/go-faster/jx"
|
||||
"github.com/ogen-go/ogen/ogenerrors"
|
||||
"github.com/ogen-go/ogen/validate"
|
||||
)
|
||||
|
||||
func (s *Server) decodeAuthApproveRequest(r *http.Request) (
|
||||
req *AuthApproveReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request AuthApproveReq
|
||||
if err := func() error {
|
||||
if err := request.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeAuthRegisterRequest(r *http.Request) (
|
||||
req *AuthRegisterReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request AuthRegisterReq
|
||||
if err := func() error {
|
||||
if err := request.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeAuthRejectRequest(r *http.Request) (
|
||||
req *AuthRejectReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request AuthRejectReq
|
||||
if err := func() error {
|
||||
if err := request.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCheckPolicyRequest(r *http.Request) (
|
||||
req *CheckPolicyReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request CheckPolicyReq
|
||||
if err := func() error {
|
||||
if err := request.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateApiKeyRequest(r *http.Request) (
|
||||
req *CreateApiKeyReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request CreateApiKeyReq
|
||||
if err := func() error {
|
||||
if err := request.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreatePreAuthKeyRequest(r *http.Request) (
|
||||
req *CreatePreAuthKeyReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request CreatePreAuthKeyReq
|
||||
if err := func() error {
|
||||
if err := request.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateUserRequest(r *http.Request) (
|
||||
req *CreateUserReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request CreateUserReq
|
||||
if err := func() error {
|
||||
if err := request.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeDebugCreateNodeRequest(r *http.Request) (
|
||||
req *DebugCreateNodeReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request DebugCreateNodeReq
|
||||
if err := func() error {
|
||||
if err := request.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeExpireApiKeyRequest(r *http.Request) (
|
||||
req *ExpireApiKeyReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request ExpireApiKeyReq
|
||||
if err := func() error {
|
||||
if err := request.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeExpirePreAuthKeyRequest(r *http.Request) (
|
||||
req *ExpirePreAuthKeyReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request ExpirePreAuthKeyReq
|
||||
if err := func() error {
|
||||
if err := request.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeSetApprovedRoutesRequest(r *http.Request) (
|
||||
req *SetApprovedRoutesReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request SetApprovedRoutesReq
|
||||
if err := func() error {
|
||||
if err := request.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeSetPolicyRequest(r *http.Request) (
|
||||
req *SetPolicyReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request SetPolicyReq
|
||||
if err := func() error {
|
||||
if err := request.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeSetTagsRequest(r *http.Request) (
|
||||
req *SetTagsReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request SetTagsReq
|
||||
if err := func() error {
|
||||
if err := request.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
// Code generated by ogen, DO NOT EDIT.
|
||||
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-faster/jx"
|
||||
ht "github.com/ogen-go/ogen/http"
|
||||
)
|
||||
|
||||
func encodeAuthApproveRequest(
|
||||
req *AuthApproveReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeAuthRegisterRequest(
|
||||
req *AuthRegisterReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeAuthRejectRequest(
|
||||
req *AuthRejectReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeCheckPolicyRequest(
|
||||
req *CheckPolicyReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeCreateApiKeyRequest(
|
||||
req *CreateApiKeyReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeCreatePreAuthKeyRequest(
|
||||
req *CreatePreAuthKeyReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeCreateUserRequest(
|
||||
req *CreateUserReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeDebugCreateNodeRequest(
|
||||
req *DebugCreateNodeReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeExpireApiKeyRequest(
|
||||
req *ExpireApiKeyReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeExpirePreAuthKeyRequest(
|
||||
req *ExpirePreAuthKeyReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeSetApprovedRoutesRequest(
|
||||
req *SetApprovedRoutesReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeSetPolicyRequest(
|
||||
req *SetPolicyReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeSetTagsRequest(
|
||||
req *SetTagsReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,415 @@
|
||||
// Code generated by ogen, DO NOT EDIT.
|
||||
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
"github.com/go-faster/jx"
|
||||
ht "github.com/ogen-go/ogen/http"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func encodeAuthApproveResponse(response *AuthApproveOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeAuthRegisterResponse(response *AuthRegisterOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeAuthRejectResponse(response *AuthRejectOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeBackfillNodeIPsResponse(response *BackfillNodeIPsOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeCheckPolicyResponse(response *CheckPolicyOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeCreateApiKeyResponse(response *CreateApiKeyOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeCreatePreAuthKeyResponse(response *CreatePreAuthKeyOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeCreateUserResponse(response *CreateUserOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeDebugCreateNodeResponse(response *DebugCreateNodeOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeDeleteApiKeyResponse(response *DeleteApiKeyOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeDeleteNodeResponse(response *DeleteNodeOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeDeletePreAuthKeyResponse(response *DeletePreAuthKeyOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeDeleteUserResponse(response *DeleteUserOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeExpireApiKeyResponse(response *ExpireApiKeyOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeExpireNodeResponse(response *ExpireNodeOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeExpirePreAuthKeyResponse(response *ExpirePreAuthKeyOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeGetNodeResponse(response *GetNodeOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeGetPolicyResponse(response *GetPolicyOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeHealthResponse(response *HealthOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeListApiKeysResponse(response *ListApiKeysOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeListNodesResponse(response *ListNodesOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeListPreAuthKeysResponse(response *ListPreAuthKeysOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeListUsersResponse(response *ListUsersOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeRegisterNodeResponse(response *RegisterNodeOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeRenameNodeResponse(response *RenameNodeOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeRenameUserResponse(response *RenameUserOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeSetApprovedRoutesResponse(response *SetApprovedRoutesOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeSetPolicyResponse(response *SetPolicyOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeSetTagsResponse(response *SetTagsOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeErrorResponse(response *ErrorStatusCode, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/problem+json")
|
||||
code := response.StatusCode
|
||||
if code == 0 {
|
||||
// Set default status code.
|
||||
code = http.StatusOK
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
if code >= http.StatusInternalServerError {
|
||||
span.SetStatus(codes.Error, http.StatusText(code))
|
||||
}
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
if code >= http.StatusInternalServerError {
|
||||
return errors.Wrapf(ht.ErrInternalServerErrorResponse, "code: %d, message: %s", code, http.StatusText(code))
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,119 @@
|
||||
// Code generated by ogen, DO NOT EDIT.
|
||||
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
"github.com/ogen-go/ogen/ogenerrors"
|
||||
)
|
||||
|
||||
// SecurityHandler is handler for security parameters.
|
||||
type SecurityHandler interface {
|
||||
// HandleBearerAuth handles bearerAuth security.
|
||||
HandleBearerAuth(ctx context.Context, operationName OperationName, t BearerAuth) (context.Context, error)
|
||||
}
|
||||
|
||||
func findAuthorization(h http.Header, prefix string) (string, bool) {
|
||||
v, ok := h["Authorization"]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
for _, vv := range v {
|
||||
scheme, value, ok := strings.Cut(vv, " ")
|
||||
if !ok || !strings.EqualFold(scheme, prefix) {
|
||||
continue
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// operationRolesBearerAuth is a private map storing roles per operation.
|
||||
var operationRolesBearerAuth = map[string][]string{
|
||||
AuthApproveOperation: []string{},
|
||||
AuthRegisterOperation: []string{},
|
||||
AuthRejectOperation: []string{},
|
||||
BackfillNodeIPsOperation: []string{},
|
||||
CheckPolicyOperation: []string{},
|
||||
CreateApiKeyOperation: []string{},
|
||||
CreatePreAuthKeyOperation: []string{},
|
||||
CreateUserOperation: []string{},
|
||||
DebugCreateNodeOperation: []string{},
|
||||
DeleteApiKeyOperation: []string{},
|
||||
DeleteNodeOperation: []string{},
|
||||
DeletePreAuthKeyOperation: []string{},
|
||||
DeleteUserOperation: []string{},
|
||||
ExpireApiKeyOperation: []string{},
|
||||
ExpireNodeOperation: []string{},
|
||||
ExpirePreAuthKeyOperation: []string{},
|
||||
GetNodeOperation: []string{},
|
||||
GetPolicyOperation: []string{},
|
||||
HealthOperation: []string{},
|
||||
ListApiKeysOperation: []string{},
|
||||
ListNodesOperation: []string{},
|
||||
ListPreAuthKeysOperation: []string{},
|
||||
ListUsersOperation: []string{},
|
||||
RegisterNodeOperation: []string{},
|
||||
RenameNodeOperation: []string{},
|
||||
RenameUserOperation: []string{},
|
||||
SetApprovedRoutesOperation: []string{},
|
||||
SetPolicyOperation: []string{},
|
||||
SetTagsOperation: []string{},
|
||||
}
|
||||
|
||||
// GetRolesForBearerAuth returns the required roles for the given operation.
|
||||
//
|
||||
// This is useful for authorization scenarios where you need to know which roles
|
||||
// are required for an operation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// requiredRoles := GetRolesForBearerAuth(AddPetOperation)
|
||||
//
|
||||
// Returns nil if the operation has no role requirements or if the operation is unknown.
|
||||
func GetRolesForBearerAuth(operation string) []string {
|
||||
roles, ok := operationRolesBearerAuth[operation]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// Return a copy to prevent external modification
|
||||
result := make([]string, len(roles))
|
||||
copy(result, roles)
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Server) securityBearerAuth(ctx context.Context, operationName OperationName, req *http.Request) (context.Context, bool, error) {
|
||||
var t BearerAuth
|
||||
token, ok := findAuthorization(req.Header, "Bearer")
|
||||
if !ok {
|
||||
return ctx, false, nil
|
||||
}
|
||||
t.Token = token
|
||||
t.Roles = operationRolesBearerAuth[operationName]
|
||||
rctx, err := s.sec.HandleBearerAuth(ctx, operationName, t)
|
||||
if errors.Is(err, ogenerrors.ErrSkipServerSecurity) {
|
||||
return nil, false, nil
|
||||
} else if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return rctx, true, err
|
||||
}
|
||||
|
||||
// SecuritySource is provider of security values (tokens, passwords, etc.).
|
||||
type SecuritySource interface {
|
||||
// BearerAuth provides bearerAuth security value.
|
||||
BearerAuth(ctx context.Context, operationName OperationName) (BearerAuth, error)
|
||||
}
|
||||
|
||||
func (s *Client) securityBearerAuth(ctx context.Context, operationName OperationName, req *http.Request) error {
|
||||
t, err := s.sec.BearerAuth(ctx, operationName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "security source \"BearerAuth\"")
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+t.Token)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// Code generated by ogen, DO NOT EDIT.
|
||||
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Handler handles operations described by OpenAPI v3 specification.
|
||||
type Handler interface {
|
||||
// AuthApprove implements AuthApprove operation.
|
||||
//
|
||||
// Approve a pending auth session.
|
||||
//
|
||||
// POST /api/v1/auth/approve
|
||||
AuthApprove(ctx context.Context, req *AuthApproveReq) error
|
||||
// AuthRegister implements AuthRegister operation.
|
||||
//
|
||||
// Register a node via an auth id (alias of RegisterNode).
|
||||
//
|
||||
// POST /api/v1/auth/register
|
||||
AuthRegister(ctx context.Context, req *AuthRegisterReq) (*AuthRegisterOK, error)
|
||||
// AuthReject implements AuthReject operation.
|
||||
//
|
||||
// Reject a pending auth session.
|
||||
//
|
||||
// POST /api/v1/auth/reject
|
||||
AuthReject(ctx context.Context, req *AuthRejectReq) error
|
||||
// BackfillNodeIPs implements BackfillNodeIPs operation.
|
||||
//
|
||||
// Backfill missing IP addresses for all nodes.
|
||||
//
|
||||
// POST /api/v1/node/backfillips
|
||||
BackfillNodeIPs(ctx context.Context, params BackfillNodeIPsParams) (*BackfillNodeIPsOK, error)
|
||||
// CheckPolicy implements CheckPolicy operation.
|
||||
//
|
||||
// Validate a policy against live users and nodes without storing it.
|
||||
//
|
||||
// POST /api/v1/policy/check
|
||||
CheckPolicy(ctx context.Context, req *CheckPolicyReq) error
|
||||
// CreateApiKey implements CreateApiKey operation.
|
||||
//
|
||||
// Create an API key.
|
||||
//
|
||||
// POST /api/v1/apikey
|
||||
CreateApiKey(ctx context.Context, req *CreateApiKeyReq) (*CreateApiKeyOK, error)
|
||||
// CreatePreAuthKey implements CreatePreAuthKey operation.
|
||||
//
|
||||
// Create a pre-auth key.
|
||||
//
|
||||
// POST /api/v1/preauthkey
|
||||
CreatePreAuthKey(ctx context.Context, req *CreatePreAuthKeyReq) (*CreatePreAuthKeyOK, error)
|
||||
// CreateUser implements CreateUser operation.
|
||||
//
|
||||
// Create a user.
|
||||
//
|
||||
// POST /api/v1/user
|
||||
CreateUser(ctx context.Context, req *CreateUserReq) (*CreateUserOK, error)
|
||||
// DebugCreateNode implements DebugCreateNode operation.
|
||||
//
|
||||
// Create a debug node and cache its registration (testing only).
|
||||
//
|
||||
// POST /api/v1/debug/node
|
||||
DebugCreateNode(ctx context.Context, req *DebugCreateNodeReq) (*DebugCreateNodeOK, error)
|
||||
// DeleteApiKey implements DeleteApiKey operation.
|
||||
//
|
||||
// Delete an API key by prefix (or id).
|
||||
//
|
||||
// DELETE /api/v1/apikey/{prefix}
|
||||
DeleteApiKey(ctx context.Context, params DeleteApiKeyParams) error
|
||||
// DeleteNode implements DeleteNode operation.
|
||||
//
|
||||
// Delete a node.
|
||||
//
|
||||
// DELETE /api/v1/node/{node_id}
|
||||
DeleteNode(ctx context.Context, params DeleteNodeParams) error
|
||||
// DeletePreAuthKey implements DeletePreAuthKey operation.
|
||||
//
|
||||
// Delete a pre-auth key.
|
||||
//
|
||||
// DELETE /api/v1/preauthkey
|
||||
DeletePreAuthKey(ctx context.Context, params DeletePreAuthKeyParams) error
|
||||
// DeleteUser implements DeleteUser operation.
|
||||
//
|
||||
// Delete a user.
|
||||
//
|
||||
// DELETE /api/v1/user/{id}
|
||||
DeleteUser(ctx context.Context, params DeleteUserParams) error
|
||||
// ExpireApiKey implements ExpireApiKey operation.
|
||||
//
|
||||
// Expire an API key by id or prefix.
|
||||
//
|
||||
// POST /api/v1/apikey/expire
|
||||
ExpireApiKey(ctx context.Context, req *ExpireApiKeyReq) error
|
||||
// ExpireNode implements ExpireNode operation.
|
||||
//
|
||||
// Expire a node, or disable its expiry.
|
||||
//
|
||||
// POST /api/v1/node/{node_id}/expire
|
||||
ExpireNode(ctx context.Context, params ExpireNodeParams) (*ExpireNodeOK, error)
|
||||
// ExpirePreAuthKey implements ExpirePreAuthKey operation.
|
||||
//
|
||||
// Expire a pre-auth key.
|
||||
//
|
||||
// POST /api/v1/preauthkey/expire
|
||||
ExpirePreAuthKey(ctx context.Context, req *ExpirePreAuthKeyReq) error
|
||||
// GetNode implements GetNode operation.
|
||||
//
|
||||
// Get a node by id.
|
||||
//
|
||||
// GET /api/v1/node/{node_id}
|
||||
GetNode(ctx context.Context, params GetNodeParams) (*GetNodeOK, error)
|
||||
// GetPolicy implements GetPolicy operation.
|
||||
//
|
||||
// Get the current ACL policy.
|
||||
//
|
||||
// GET /api/v1/policy
|
||||
GetPolicy(ctx context.Context) (*GetPolicyOK, error)
|
||||
// Health implements Health operation.
|
||||
//
|
||||
// Report server health, including database connectivity.
|
||||
//
|
||||
// GET /api/v1/health
|
||||
Health(ctx context.Context) (*HealthOK, error)
|
||||
// ListApiKeys implements ListApiKeys operation.
|
||||
//
|
||||
// List all API keys.
|
||||
//
|
||||
// GET /api/v1/apikey
|
||||
ListApiKeys(ctx context.Context) (*ListApiKeysOK, error)
|
||||
// ListNodes implements ListNodes operation.
|
||||
//
|
||||
// List nodes, optionally filtered by user.
|
||||
//
|
||||
// GET /api/v1/node
|
||||
ListNodes(ctx context.Context, params ListNodesParams) (*ListNodesOK, error)
|
||||
// ListPreAuthKeys implements ListPreAuthKeys operation.
|
||||
//
|
||||
// List all pre-auth keys.
|
||||
//
|
||||
// GET /api/v1/preauthkey
|
||||
ListPreAuthKeys(ctx context.Context) (*ListPreAuthKeysOK, error)
|
||||
// ListUsers implements ListUsers operation.
|
||||
//
|
||||
// List users, optionally filtered by id, name, or email.
|
||||
//
|
||||
// GET /api/v1/user
|
||||
ListUsers(ctx context.Context, params ListUsersParams) (*ListUsersOK, error)
|
||||
// RegisterNode implements RegisterNode operation.
|
||||
//
|
||||
// Register a node to a user using a registration id.
|
||||
//
|
||||
// POST /api/v1/node/register
|
||||
RegisterNode(ctx context.Context, params RegisterNodeParams) (*RegisterNodeOK, error)
|
||||
// RenameNode implements RenameNode operation.
|
||||
//
|
||||
// Rename a node.
|
||||
//
|
||||
// POST /api/v1/node/{node_id}/rename/{new_name}
|
||||
RenameNode(ctx context.Context, params RenameNodeParams) (*RenameNodeOK, error)
|
||||
// RenameUser implements RenameUser operation.
|
||||
//
|
||||
// Rename a user.
|
||||
//
|
||||
// POST /api/v1/user/{old_id}/rename/{new_name}
|
||||
RenameUser(ctx context.Context, params RenameUserParams) (*RenameUserOK, error)
|
||||
// SetApprovedRoutes implements SetApprovedRoutes operation.
|
||||
//
|
||||
// Set the approved subnet routes of a node.
|
||||
//
|
||||
// POST /api/v1/node/{node_id}/approve_routes
|
||||
SetApprovedRoutes(ctx context.Context, req *SetApprovedRoutesReq, params SetApprovedRoutesParams) (*SetApprovedRoutesOK, error)
|
||||
// SetPolicy implements SetPolicy operation.
|
||||
//
|
||||
// Set the ACL policy (database policy mode only).
|
||||
//
|
||||
// PUT /api/v1/policy
|
||||
SetPolicy(ctx context.Context, req *SetPolicyReq) (*SetPolicyOK, error)
|
||||
// SetTags implements SetTags operation.
|
||||
//
|
||||
// Set the tags of a node (converts it to a tagged node).
|
||||
//
|
||||
// POST /api/v1/node/{node_id}/tags
|
||||
SetTags(ctx context.Context, req *SetTagsReq, params SetTagsParams) (*SetTagsOK, error)
|
||||
// NewError creates *ErrorStatusCode from error returned by handler.
|
||||
//
|
||||
// Used for common default response.
|
||||
NewError(ctx context.Context, err error) *ErrorStatusCode
|
||||
}
|
||||
|
||||
// Server implements http server based on OpenAPI v3 specification and
|
||||
// calls Handler to handle requests.
|
||||
type Server struct {
|
||||
h Handler
|
||||
sec SecurityHandler
|
||||
baseServer
|
||||
}
|
||||
|
||||
// NewServer creates new Server.
|
||||
func NewServer(h Handler, sec SecurityHandler, opts ...ServerOption) (*Server, error) {
|
||||
s, err := newServerConfig(opts...).baseServer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Server{
|
||||
h: h,
|
||||
sec: sec,
|
||||
baseServer: s,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// Code generated by ogen, DO NOT EDIT.
|
||||
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
ht "github.com/ogen-go/ogen/http"
|
||||
)
|
||||
|
||||
// UnimplementedHandler is no-op Handler which returns http.ErrNotImplemented.
|
||||
type UnimplementedHandler struct{}
|
||||
|
||||
var _ Handler = UnimplementedHandler{}
|
||||
|
||||
// AuthApprove implements AuthApprove operation.
|
||||
//
|
||||
// Approve a pending auth session.
|
||||
//
|
||||
// POST /api/v1/auth/approve
|
||||
func (UnimplementedHandler) AuthApprove(ctx context.Context, req *AuthApproveReq) error {
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// AuthRegister implements AuthRegister operation.
|
||||
//
|
||||
// Register a node via an auth id (alias of RegisterNode).
|
||||
//
|
||||
// POST /api/v1/auth/register
|
||||
func (UnimplementedHandler) AuthRegister(ctx context.Context, req *AuthRegisterReq) (r *AuthRegisterOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// AuthReject implements AuthReject operation.
|
||||
//
|
||||
// Reject a pending auth session.
|
||||
//
|
||||
// POST /api/v1/auth/reject
|
||||
func (UnimplementedHandler) AuthReject(ctx context.Context, req *AuthRejectReq) error {
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// BackfillNodeIPs implements BackfillNodeIPs operation.
|
||||
//
|
||||
// Backfill missing IP addresses for all nodes.
|
||||
//
|
||||
// POST /api/v1/node/backfillips
|
||||
func (UnimplementedHandler) BackfillNodeIPs(ctx context.Context, params BackfillNodeIPsParams) (r *BackfillNodeIPsOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// CheckPolicy implements CheckPolicy operation.
|
||||
//
|
||||
// Validate a policy against live users and nodes without storing it.
|
||||
//
|
||||
// POST /api/v1/policy/check
|
||||
func (UnimplementedHandler) CheckPolicy(ctx context.Context, req *CheckPolicyReq) error {
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// CreateApiKey implements CreateApiKey operation.
|
||||
//
|
||||
// Create an API key.
|
||||
//
|
||||
// POST /api/v1/apikey
|
||||
func (UnimplementedHandler) CreateApiKey(ctx context.Context, req *CreateApiKeyReq) (r *CreateApiKeyOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// CreatePreAuthKey implements CreatePreAuthKey operation.
|
||||
//
|
||||
// Create a pre-auth key.
|
||||
//
|
||||
// POST /api/v1/preauthkey
|
||||
func (UnimplementedHandler) CreatePreAuthKey(ctx context.Context, req *CreatePreAuthKeyReq) (r *CreatePreAuthKeyOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// CreateUser implements CreateUser operation.
|
||||
//
|
||||
// Create a user.
|
||||
//
|
||||
// POST /api/v1/user
|
||||
func (UnimplementedHandler) CreateUser(ctx context.Context, req *CreateUserReq) (r *CreateUserOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// DebugCreateNode implements DebugCreateNode operation.
|
||||
//
|
||||
// Create a debug node and cache its registration (testing only).
|
||||
//
|
||||
// POST /api/v1/debug/node
|
||||
func (UnimplementedHandler) DebugCreateNode(ctx context.Context, req *DebugCreateNodeReq) (r *DebugCreateNodeOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// DeleteApiKey implements DeleteApiKey operation.
|
||||
//
|
||||
// Delete an API key by prefix (or id).
|
||||
//
|
||||
// DELETE /api/v1/apikey/{prefix}
|
||||
func (UnimplementedHandler) DeleteApiKey(ctx context.Context, params DeleteApiKeyParams) error {
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// DeleteNode implements DeleteNode operation.
|
||||
//
|
||||
// Delete a node.
|
||||
//
|
||||
// DELETE /api/v1/node/{node_id}
|
||||
func (UnimplementedHandler) DeleteNode(ctx context.Context, params DeleteNodeParams) error {
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// DeletePreAuthKey implements DeletePreAuthKey operation.
|
||||
//
|
||||
// Delete a pre-auth key.
|
||||
//
|
||||
// DELETE /api/v1/preauthkey
|
||||
func (UnimplementedHandler) DeletePreAuthKey(ctx context.Context, params DeletePreAuthKeyParams) error {
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// DeleteUser implements DeleteUser operation.
|
||||
//
|
||||
// Delete a user.
|
||||
//
|
||||
// DELETE /api/v1/user/{id}
|
||||
func (UnimplementedHandler) DeleteUser(ctx context.Context, params DeleteUserParams) error {
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ExpireApiKey implements ExpireApiKey operation.
|
||||
//
|
||||
// Expire an API key by id or prefix.
|
||||
//
|
||||
// POST /api/v1/apikey/expire
|
||||
func (UnimplementedHandler) ExpireApiKey(ctx context.Context, req *ExpireApiKeyReq) error {
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ExpireNode implements ExpireNode operation.
|
||||
//
|
||||
// Expire a node, or disable its expiry.
|
||||
//
|
||||
// POST /api/v1/node/{node_id}/expire
|
||||
func (UnimplementedHandler) ExpireNode(ctx context.Context, params ExpireNodeParams) (r *ExpireNodeOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ExpirePreAuthKey implements ExpirePreAuthKey operation.
|
||||
//
|
||||
// Expire a pre-auth key.
|
||||
//
|
||||
// POST /api/v1/preauthkey/expire
|
||||
func (UnimplementedHandler) ExpirePreAuthKey(ctx context.Context, req *ExpirePreAuthKeyReq) error {
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// GetNode implements GetNode operation.
|
||||
//
|
||||
// Get a node by id.
|
||||
//
|
||||
// GET /api/v1/node/{node_id}
|
||||
func (UnimplementedHandler) GetNode(ctx context.Context, params GetNodeParams) (r *GetNodeOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// GetPolicy implements GetPolicy operation.
|
||||
//
|
||||
// Get the current ACL policy.
|
||||
//
|
||||
// GET /api/v1/policy
|
||||
func (UnimplementedHandler) GetPolicy(ctx context.Context) (r *GetPolicyOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// Health implements Health operation.
|
||||
//
|
||||
// Report server health, including database connectivity.
|
||||
//
|
||||
// GET /api/v1/health
|
||||
func (UnimplementedHandler) Health(ctx context.Context) (r *HealthOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ListApiKeys implements ListApiKeys operation.
|
||||
//
|
||||
// List all API keys.
|
||||
//
|
||||
// GET /api/v1/apikey
|
||||
func (UnimplementedHandler) ListApiKeys(ctx context.Context) (r *ListApiKeysOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ListNodes implements ListNodes operation.
|
||||
//
|
||||
// List nodes, optionally filtered by user.
|
||||
//
|
||||
// GET /api/v1/node
|
||||
func (UnimplementedHandler) ListNodes(ctx context.Context, params ListNodesParams) (r *ListNodesOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ListPreAuthKeys implements ListPreAuthKeys operation.
|
||||
//
|
||||
// List all pre-auth keys.
|
||||
//
|
||||
// GET /api/v1/preauthkey
|
||||
func (UnimplementedHandler) ListPreAuthKeys(ctx context.Context) (r *ListPreAuthKeysOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ListUsers implements ListUsers operation.
|
||||
//
|
||||
// List users, optionally filtered by id, name, or email.
|
||||
//
|
||||
// GET /api/v1/user
|
||||
func (UnimplementedHandler) ListUsers(ctx context.Context, params ListUsersParams) (r *ListUsersOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// RegisterNode implements RegisterNode operation.
|
||||
//
|
||||
// Register a node to a user using a registration id.
|
||||
//
|
||||
// POST /api/v1/node/register
|
||||
func (UnimplementedHandler) RegisterNode(ctx context.Context, params RegisterNodeParams) (r *RegisterNodeOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// RenameNode implements RenameNode operation.
|
||||
//
|
||||
// Rename a node.
|
||||
//
|
||||
// POST /api/v1/node/{node_id}/rename/{new_name}
|
||||
func (UnimplementedHandler) RenameNode(ctx context.Context, params RenameNodeParams) (r *RenameNodeOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// RenameUser implements RenameUser operation.
|
||||
//
|
||||
// Rename a user.
|
||||
//
|
||||
// POST /api/v1/user/{old_id}/rename/{new_name}
|
||||
func (UnimplementedHandler) RenameUser(ctx context.Context, params RenameUserParams) (r *RenameUserOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// SetApprovedRoutes implements SetApprovedRoutes operation.
|
||||
//
|
||||
// Set the approved subnet routes of a node.
|
||||
//
|
||||
// POST /api/v1/node/{node_id}/approve_routes
|
||||
func (UnimplementedHandler) SetApprovedRoutes(ctx context.Context, req *SetApprovedRoutesReq, params SetApprovedRoutesParams) (r *SetApprovedRoutesOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// SetPolicy implements SetPolicy operation.
|
||||
//
|
||||
// Set the ACL policy (database policy mode only).
|
||||
//
|
||||
// PUT /api/v1/policy
|
||||
func (UnimplementedHandler) SetPolicy(ctx context.Context, req *SetPolicyReq) (r *SetPolicyOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// SetTags implements SetTags operation.
|
||||
//
|
||||
// Set the tags of a node (converts it to a tagged node).
|
||||
//
|
||||
// POST /api/v1/node/{node_id}/tags
|
||||
func (UnimplementedHandler) SetTags(ctx context.Context, req *SetTagsReq, params SetTagsParams) (r *SetTagsOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// NewError creates *ErrorStatusCode from error returned by handler.
|
||||
//
|
||||
// Used for common default response.
|
||||
func (UnimplementedHandler) NewError(ctx context.Context, err error) (r *ErrorStatusCode) {
|
||||
r = new(ErrorStatusCode)
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// Code generated by ogen, DO NOT EDIT.
|
||||
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
"github.com/ogen-go/ogen/validate"
|
||||
)
|
||||
|
||||
func (s *AuthRegisterOK) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
}
|
||||
|
||||
var failures []validate.FieldError
|
||||
if err := func() error {
|
||||
if value, ok := s.Node.Get(); ok {
|
||||
if err := func() error {
|
||||
if err := value.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "node",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DebugCreateNodeOK) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
}
|
||||
|
||||
var failures []validate.FieldError
|
||||
if err := func() error {
|
||||
if value, ok := s.Node.Get(); ok {
|
||||
if err := func() error {
|
||||
if err := value.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "node",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ExpireNodeOK) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
}
|
||||
|
||||
var failures []validate.FieldError
|
||||
if err := func() error {
|
||||
if value, ok := s.Node.Get(); ok {
|
||||
if err := func() error {
|
||||
if err := value.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "node",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *GetNodeOK) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
}
|
||||
|
||||
var failures []validate.FieldError
|
||||
if err := func() error {
|
||||
if value, ok := s.Node.Get(); ok {
|
||||
if err := func() error {
|
||||
if err := value.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "node",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ListNodesOK) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
}
|
||||
|
||||
var failures []validate.FieldError
|
||||
if err := func() error {
|
||||
var failures []validate.FieldError
|
||||
for i, elem := range s.Nodes {
|
||||
if err := func() error {
|
||||
if err := elem.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: fmt.Sprintf("[%d]", i),
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "nodes",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Node) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
}
|
||||
|
||||
var failures []validate.FieldError
|
||||
if err := func() error {
|
||||
if value, ok := s.RegisterMethod.Get(); ok {
|
||||
if err := func() error {
|
||||
if err := value.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "registerMethod",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s RegisterMethod) Validate() error {
|
||||
switch s {
|
||||
case "REGISTER_METHOD_UNSPECIFIED":
|
||||
return nil
|
||||
case "REGISTER_METHOD_AUTH_KEY":
|
||||
return nil
|
||||
case "REGISTER_METHOD_CLI":
|
||||
return nil
|
||||
case "REGISTER_METHOD_OIDC":
|
||||
return nil
|
||||
default:
|
||||
return errors.Errorf("invalid value: %v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RegisterNodeOK) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
}
|
||||
|
||||
var failures []validate.FieldError
|
||||
if err := func() error {
|
||||
if value, ok := s.Node.Get(); ok {
|
||||
if err := func() error {
|
||||
if err := value.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "node",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RenameNodeOK) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
}
|
||||
|
||||
var failures []validate.FieldError
|
||||
if err := func() error {
|
||||
if value, ok := s.Node.Get(); ok {
|
||||
if err := func() error {
|
||||
if err := value.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "node",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SetApprovedRoutesOK) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
}
|
||||
|
||||
var failures []validate.FieldError
|
||||
if err := func() error {
|
||||
if value, ok := s.Node.Get(); ok {
|
||||
if err := func() error {
|
||||
if err := value.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "node",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SetTagsOK) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
}
|
||||
|
||||
var failures []validate.FieldError
|
||||
if err := func() error {
|
||||
if value, ok := s.Node.Get(); ok {
|
||||
if err := func() error {
|
||||
if err := value.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "node",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,537 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/apikey.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type ApiKey struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Prefix string `protobuf:"bytes,2,opt,name=prefix,proto3" json:"prefix,omitempty"`
|
||||
Expiration *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=expiration,proto3" json:"expiration,omitempty"`
|
||||
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
LastSeen *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ApiKey) Reset() {
|
||||
*x = ApiKey{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ApiKey) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ApiKey) ProtoMessage() {}
|
||||
|
||||
func (x *ApiKey) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ApiKey.ProtoReflect.Descriptor instead.
|
||||
func (*ApiKey) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *ApiKey) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ApiKey) GetPrefix() string {
|
||||
if x != nil {
|
||||
return x.Prefix
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ApiKey) GetExpiration() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.Expiration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ApiKey) GetCreatedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.CreatedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ApiKey) GetLastSeen() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.LastSeen
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CreateApiKeyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Expiration *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=expiration,proto3" json:"expiration,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreateApiKeyRequest) Reset() {
|
||||
*x = CreateApiKeyRequest{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreateApiKeyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreateApiKeyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CreateApiKeyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreateApiKeyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CreateApiKeyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *CreateApiKeyRequest) GetExpiration() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.Expiration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CreateApiKeyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ApiKey string `protobuf:"bytes,1,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreateApiKeyResponse) Reset() {
|
||||
*x = CreateApiKeyResponse{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreateApiKeyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreateApiKeyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CreateApiKeyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreateApiKeyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CreateApiKeyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *CreateApiKeyResponse) GetApiKey() string {
|
||||
if x != nil {
|
||||
return x.ApiKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ExpireApiKeyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"`
|
||||
Id uint64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ExpireApiKeyRequest) Reset() {
|
||||
*x = ExpireApiKeyRequest{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExpireApiKeyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExpireApiKeyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ExpireApiKeyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExpireApiKeyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ExpireApiKeyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *ExpireApiKeyRequest) GetPrefix() string {
|
||||
if x != nil {
|
||||
return x.Prefix
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ExpireApiKeyRequest) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ExpireApiKeyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ExpireApiKeyResponse) Reset() {
|
||||
*x = ExpireApiKeyResponse{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExpireApiKeyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExpireApiKeyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ExpireApiKeyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExpireApiKeyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ExpireApiKeyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
type ListApiKeysRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListApiKeysRequest) Reset() {
|
||||
*x = ListApiKeysRequest{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListApiKeysRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListApiKeysRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ListApiKeysRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListApiKeysRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ListApiKeysRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
type ListApiKeysResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ApiKeys []*ApiKey `protobuf:"bytes,1,rep,name=api_keys,json=apiKeys,proto3" json:"api_keys,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListApiKeysResponse) Reset() {
|
||||
*x = ListApiKeysResponse{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListApiKeysResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListApiKeysResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ListApiKeysResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListApiKeysResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ListApiKeysResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *ListApiKeysResponse) GetApiKeys() []*ApiKey {
|
||||
if x != nil {
|
||||
return x.ApiKeys
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeleteApiKeyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"`
|
||||
Id uint64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteApiKeyRequest) Reset() {
|
||||
*x = DeleteApiKeyRequest{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeleteApiKeyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeleteApiKeyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DeleteApiKeyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeleteApiKeyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DeleteApiKeyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *DeleteApiKeyRequest) GetPrefix() string {
|
||||
if x != nil {
|
||||
return x.Prefix
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *DeleteApiKeyRequest) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type DeleteApiKeyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteApiKeyResponse) Reset() {
|
||||
*x = DeleteApiKeyResponse{}
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeleteApiKeyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeleteApiKeyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *DeleteApiKeyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_apikey_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeleteApiKeyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*DeleteApiKeyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
var File_headscale_v1_apikey_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_headscale_v1_apikey_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x19headscale/v1/apikey.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe0\x01\n" +
|
||||
"\x06ApiKey\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x04R\x02id\x12\x16\n" +
|
||||
"\x06prefix\x18\x02 \x01(\tR\x06prefix\x12:\n" +
|
||||
"\n" +
|
||||
"expiration\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
|
||||
"expiration\x129\n" +
|
||||
"\n" +
|
||||
"created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x127\n" +
|
||||
"\tlast_seen\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\blastSeen\"Q\n" +
|
||||
"\x13CreateApiKeyRequest\x12:\n" +
|
||||
"\n" +
|
||||
"expiration\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
|
||||
"expiration\"/\n" +
|
||||
"\x14CreateApiKeyResponse\x12\x17\n" +
|
||||
"\aapi_key\x18\x01 \x01(\tR\x06apiKey\"=\n" +
|
||||
"\x13ExpireApiKeyRequest\x12\x16\n" +
|
||||
"\x06prefix\x18\x01 \x01(\tR\x06prefix\x12\x0e\n" +
|
||||
"\x02id\x18\x02 \x01(\x04R\x02id\"\x16\n" +
|
||||
"\x14ExpireApiKeyResponse\"\x14\n" +
|
||||
"\x12ListApiKeysRequest\"F\n" +
|
||||
"\x13ListApiKeysResponse\x12/\n" +
|
||||
"\bapi_keys\x18\x01 \x03(\v2\x14.headscale.v1.ApiKeyR\aapiKeys\"=\n" +
|
||||
"\x13DeleteApiKeyRequest\x12\x16\n" +
|
||||
"\x06prefix\x18\x01 \x01(\tR\x06prefix\x12\x0e\n" +
|
||||
"\x02id\x18\x02 \x01(\x04R\x02id\"\x16\n" +
|
||||
"\x14DeleteApiKeyResponseB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_headscale_v1_apikey_proto_rawDescOnce sync.Once
|
||||
file_headscale_v1_apikey_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_headscale_v1_apikey_proto_rawDescGZIP() []byte {
|
||||
file_headscale_v1_apikey_proto_rawDescOnce.Do(func() {
|
||||
file_headscale_v1_apikey_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_apikey_proto_rawDesc), len(file_headscale_v1_apikey_proto_rawDesc)))
|
||||
})
|
||||
return file_headscale_v1_apikey_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_headscale_v1_apikey_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_headscale_v1_apikey_proto_goTypes = []any{
|
||||
(*ApiKey)(nil), // 0: headscale.v1.ApiKey
|
||||
(*CreateApiKeyRequest)(nil), // 1: headscale.v1.CreateApiKeyRequest
|
||||
(*CreateApiKeyResponse)(nil), // 2: headscale.v1.CreateApiKeyResponse
|
||||
(*ExpireApiKeyRequest)(nil), // 3: headscale.v1.ExpireApiKeyRequest
|
||||
(*ExpireApiKeyResponse)(nil), // 4: headscale.v1.ExpireApiKeyResponse
|
||||
(*ListApiKeysRequest)(nil), // 5: headscale.v1.ListApiKeysRequest
|
||||
(*ListApiKeysResponse)(nil), // 6: headscale.v1.ListApiKeysResponse
|
||||
(*DeleteApiKeyRequest)(nil), // 7: headscale.v1.DeleteApiKeyRequest
|
||||
(*DeleteApiKeyResponse)(nil), // 8: headscale.v1.DeleteApiKeyResponse
|
||||
(*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp
|
||||
}
|
||||
var file_headscale_v1_apikey_proto_depIdxs = []int32{
|
||||
9, // 0: headscale.v1.ApiKey.expiration:type_name -> google.protobuf.Timestamp
|
||||
9, // 1: headscale.v1.ApiKey.created_at:type_name -> google.protobuf.Timestamp
|
||||
9, // 2: headscale.v1.ApiKey.last_seen:type_name -> google.protobuf.Timestamp
|
||||
9, // 3: headscale.v1.CreateApiKeyRequest.expiration:type_name -> google.protobuf.Timestamp
|
||||
0, // 4: headscale.v1.ListApiKeysResponse.api_keys:type_name -> headscale.v1.ApiKey
|
||||
5, // [5:5] is the sub-list for method output_type
|
||||
5, // [5:5] is the sub-list for method input_type
|
||||
5, // [5:5] is the sub-list for extension type_name
|
||||
5, // [5:5] is the sub-list for extension extendee
|
||||
0, // [0:5] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_apikey_proto_init() }
|
||||
func file_headscale_v1_apikey_proto_init() {
|
||||
if File_headscale_v1_apikey_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_apikey_proto_rawDesc), len(file_headscale_v1_apikey_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 9,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_headscale_v1_apikey_proto_goTypes,
|
||||
DependencyIndexes: file_headscale_v1_apikey_proto_depIdxs,
|
||||
MessageInfos: file_headscale_v1_apikey_proto_msgTypes,
|
||||
}.Build()
|
||||
File_headscale_v1_apikey_proto = out.File
|
||||
file_headscale_v1_apikey_proto_goTypes = nil
|
||||
file_headscale_v1_apikey_proto_depIdxs = nil
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/auth.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type AuthRegisterRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
AuthId string `protobuf:"bytes,2,opt,name=auth_id,json=authId,proto3" json:"auth_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthRegisterRequest) Reset() {
|
||||
*x = AuthRegisterRequest{}
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthRegisterRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthRegisterRequest) ProtoMessage() {}
|
||||
|
||||
func (x *AuthRegisterRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthRegisterRequest.ProtoReflect.Descriptor instead.
|
||||
func (*AuthRegisterRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *AuthRegisterRequest) GetUser() string {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthRegisterRequest) GetAuthId() string {
|
||||
if x != nil {
|
||||
return x.AuthId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type AuthRegisterResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Node *Node `protobuf:"bytes,1,opt,name=node,proto3" json:"node,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthRegisterResponse) Reset() {
|
||||
*x = AuthRegisterResponse{}
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthRegisterResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthRegisterResponse) ProtoMessage() {}
|
||||
|
||||
func (x *AuthRegisterResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthRegisterResponse.ProtoReflect.Descriptor instead.
|
||||
func (*AuthRegisterResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *AuthRegisterResponse) GetNode() *Node {
|
||||
if x != nil {
|
||||
return x.Node
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AuthApproveRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
AuthId string `protobuf:"bytes,1,opt,name=auth_id,json=authId,proto3" json:"auth_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthApproveRequest) Reset() {
|
||||
*x = AuthApproveRequest{}
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthApproveRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthApproveRequest) ProtoMessage() {}
|
||||
|
||||
func (x *AuthApproveRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthApproveRequest.ProtoReflect.Descriptor instead.
|
||||
func (*AuthApproveRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *AuthApproveRequest) GetAuthId() string {
|
||||
if x != nil {
|
||||
return x.AuthId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type AuthApproveResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthApproveResponse) Reset() {
|
||||
*x = AuthApproveResponse{}
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthApproveResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthApproveResponse) ProtoMessage() {}
|
||||
|
||||
func (x *AuthApproveResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthApproveResponse.ProtoReflect.Descriptor instead.
|
||||
func (*AuthApproveResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
type AuthRejectRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
AuthId string `protobuf:"bytes,1,opt,name=auth_id,json=authId,proto3" json:"auth_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthRejectRequest) Reset() {
|
||||
*x = AuthRejectRequest{}
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthRejectRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthRejectRequest) ProtoMessage() {}
|
||||
|
||||
func (x *AuthRejectRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthRejectRequest.ProtoReflect.Descriptor instead.
|
||||
func (*AuthRejectRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *AuthRejectRequest) GetAuthId() string {
|
||||
if x != nil {
|
||||
return x.AuthId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type AuthRejectResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthRejectResponse) Reset() {
|
||||
*x = AuthRejectResponse{}
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthRejectResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthRejectResponse) ProtoMessage() {}
|
||||
|
||||
func (x *AuthRejectResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_auth_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthRejectResponse.ProtoReflect.Descriptor instead.
|
||||
func (*AuthRejectResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
var File_headscale_v1_auth_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_headscale_v1_auth_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x17headscale/v1/auth.proto\x12\fheadscale.v1\x1a\x17headscale/v1/node.proto\"B\n" +
|
||||
"\x13AuthRegisterRequest\x12\x12\n" +
|
||||
"\x04user\x18\x01 \x01(\tR\x04user\x12\x17\n" +
|
||||
"\aauth_id\x18\x02 \x01(\tR\x06authId\">\n" +
|
||||
"\x14AuthRegisterResponse\x12&\n" +
|
||||
"\x04node\x18\x01 \x01(\v2\x12.headscale.v1.NodeR\x04node\"-\n" +
|
||||
"\x12AuthApproveRequest\x12\x17\n" +
|
||||
"\aauth_id\x18\x01 \x01(\tR\x06authId\"\x15\n" +
|
||||
"\x13AuthApproveResponse\",\n" +
|
||||
"\x11AuthRejectRequest\x12\x17\n" +
|
||||
"\aauth_id\x18\x01 \x01(\tR\x06authId\"\x14\n" +
|
||||
"\x12AuthRejectResponseB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_headscale_v1_auth_proto_rawDescOnce sync.Once
|
||||
file_headscale_v1_auth_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_headscale_v1_auth_proto_rawDescGZIP() []byte {
|
||||
file_headscale_v1_auth_proto_rawDescOnce.Do(func() {
|
||||
file_headscale_v1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_auth_proto_rawDesc), len(file_headscale_v1_auth_proto_rawDesc)))
|
||||
})
|
||||
return file_headscale_v1_auth_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_headscale_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||
var file_headscale_v1_auth_proto_goTypes = []any{
|
||||
(*AuthRegisterRequest)(nil), // 0: headscale.v1.AuthRegisterRequest
|
||||
(*AuthRegisterResponse)(nil), // 1: headscale.v1.AuthRegisterResponse
|
||||
(*AuthApproveRequest)(nil), // 2: headscale.v1.AuthApproveRequest
|
||||
(*AuthApproveResponse)(nil), // 3: headscale.v1.AuthApproveResponse
|
||||
(*AuthRejectRequest)(nil), // 4: headscale.v1.AuthRejectRequest
|
||||
(*AuthRejectResponse)(nil), // 5: headscale.v1.AuthRejectResponse
|
||||
(*Node)(nil), // 6: headscale.v1.Node
|
||||
}
|
||||
var file_headscale_v1_auth_proto_depIdxs = []int32{
|
||||
6, // 0: headscale.v1.AuthRegisterResponse.node:type_name -> headscale.v1.Node
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_auth_proto_init() }
|
||||
func file_headscale_v1_auth_proto_init() {
|
||||
if File_headscale_v1_auth_proto != nil {
|
||||
return
|
||||
}
|
||||
file_headscale_v1_node_proto_init()
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_auth_proto_rawDesc), len(file_headscale_v1_auth_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 6,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_headscale_v1_auth_proto_goTypes,
|
||||
DependencyIndexes: file_headscale_v1_auth_proto_depIdxs,
|
||||
MessageInfos: file_headscale_v1_auth_proto_msgTypes,
|
||||
}.Build()
|
||||
File_headscale_v1_auth_proto = out.File
|
||||
file_headscale_v1_auth_proto_goTypes = nil
|
||||
file_headscale_v1_auth_proto_depIdxs = nil
|
||||
}
|
||||
@@ -1,890 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/device.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Latency struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
LatencyMs float32 `protobuf:"fixed32,1,opt,name=latency_ms,json=latencyMs,proto3" json:"latency_ms,omitempty"`
|
||||
Preferred bool `protobuf:"varint,2,opt,name=preferred,proto3" json:"preferred,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Latency) Reset() {
|
||||
*x = Latency{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Latency) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Latency) ProtoMessage() {}
|
||||
|
||||
func (x *Latency) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Latency.ProtoReflect.Descriptor instead.
|
||||
func (*Latency) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Latency) GetLatencyMs() float32 {
|
||||
if x != nil {
|
||||
return x.LatencyMs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Latency) GetPreferred() bool {
|
||||
if x != nil {
|
||||
return x.Preferred
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ClientSupports struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
HairPinning bool `protobuf:"varint,1,opt,name=hair_pinning,json=hairPinning,proto3" json:"hair_pinning,omitempty"`
|
||||
Ipv6 bool `protobuf:"varint,2,opt,name=ipv6,proto3" json:"ipv6,omitempty"`
|
||||
Pcp bool `protobuf:"varint,3,opt,name=pcp,proto3" json:"pcp,omitempty"`
|
||||
Pmp bool `protobuf:"varint,4,opt,name=pmp,proto3" json:"pmp,omitempty"`
|
||||
Udp bool `protobuf:"varint,5,opt,name=udp,proto3" json:"udp,omitempty"`
|
||||
Upnp bool `protobuf:"varint,6,opt,name=upnp,proto3" json:"upnp,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ClientSupports) Reset() {
|
||||
*x = ClientSupports{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ClientSupports) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ClientSupports) ProtoMessage() {}
|
||||
|
||||
func (x *ClientSupports) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ClientSupports.ProtoReflect.Descriptor instead.
|
||||
func (*ClientSupports) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *ClientSupports) GetHairPinning() bool {
|
||||
if x != nil {
|
||||
return x.HairPinning
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *ClientSupports) GetIpv6() bool {
|
||||
if x != nil {
|
||||
return x.Ipv6
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *ClientSupports) GetPcp() bool {
|
||||
if x != nil {
|
||||
return x.Pcp
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *ClientSupports) GetPmp() bool {
|
||||
if x != nil {
|
||||
return x.Pmp
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *ClientSupports) GetUdp() bool {
|
||||
if x != nil {
|
||||
return x.Udp
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *ClientSupports) GetUpnp() bool {
|
||||
if x != nil {
|
||||
return x.Upnp
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ClientConnectivity struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Endpoints []string `protobuf:"bytes,1,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
|
||||
Derp string `protobuf:"bytes,2,opt,name=derp,proto3" json:"derp,omitempty"`
|
||||
MappingVariesByDestIp bool `protobuf:"varint,3,opt,name=mapping_varies_by_dest_ip,json=mappingVariesByDestIp,proto3" json:"mapping_varies_by_dest_ip,omitempty"`
|
||||
Latency map[string]*Latency `protobuf:"bytes,4,rep,name=latency,proto3" json:"latency,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
ClientSupports *ClientSupports `protobuf:"bytes,5,opt,name=client_supports,json=clientSupports,proto3" json:"client_supports,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ClientConnectivity) Reset() {
|
||||
*x = ClientConnectivity{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ClientConnectivity) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ClientConnectivity) ProtoMessage() {}
|
||||
|
||||
func (x *ClientConnectivity) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ClientConnectivity.ProtoReflect.Descriptor instead.
|
||||
func (*ClientConnectivity) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *ClientConnectivity) GetEndpoints() []string {
|
||||
if x != nil {
|
||||
return x.Endpoints
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ClientConnectivity) GetDerp() string {
|
||||
if x != nil {
|
||||
return x.Derp
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ClientConnectivity) GetMappingVariesByDestIp() bool {
|
||||
if x != nil {
|
||||
return x.MappingVariesByDestIp
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *ClientConnectivity) GetLatency() map[string]*Latency {
|
||||
if x != nil {
|
||||
return x.Latency
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ClientConnectivity) GetClientSupports() *ClientSupports {
|
||||
if x != nil {
|
||||
return x.ClientSupports
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetDeviceRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetDeviceRequest) Reset() {
|
||||
*x = GetDeviceRequest{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetDeviceRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetDeviceRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetDeviceRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetDeviceRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetDeviceRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *GetDeviceRequest) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type GetDeviceResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Addresses []string `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses,omitempty"`
|
||||
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
|
||||
User string `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Hostname string `protobuf:"bytes,5,opt,name=hostname,proto3" json:"hostname,omitempty"`
|
||||
ClientVersion string `protobuf:"bytes,6,opt,name=client_version,json=clientVersion,proto3" json:"client_version,omitempty"`
|
||||
UpdateAvailable bool `protobuf:"varint,7,opt,name=update_available,json=updateAvailable,proto3" json:"update_available,omitempty"`
|
||||
Os string `protobuf:"bytes,8,opt,name=os,proto3" json:"os,omitempty"`
|
||||
Created *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created,proto3" json:"created,omitempty"`
|
||||
LastSeen *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"`
|
||||
KeyExpiryDisabled bool `protobuf:"varint,11,opt,name=key_expiry_disabled,json=keyExpiryDisabled,proto3" json:"key_expiry_disabled,omitempty"`
|
||||
Expires *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=expires,proto3" json:"expires,omitempty"`
|
||||
Authorized bool `protobuf:"varint,13,opt,name=authorized,proto3" json:"authorized,omitempty"`
|
||||
IsExternal bool `protobuf:"varint,14,opt,name=is_external,json=isExternal,proto3" json:"is_external,omitempty"`
|
||||
MachineKey string `protobuf:"bytes,15,opt,name=machine_key,json=machineKey,proto3" json:"machine_key,omitempty"`
|
||||
NodeKey string `protobuf:"bytes,16,opt,name=node_key,json=nodeKey,proto3" json:"node_key,omitempty"`
|
||||
BlocksIncomingConnections bool `protobuf:"varint,17,opt,name=blocks_incoming_connections,json=blocksIncomingConnections,proto3" json:"blocks_incoming_connections,omitempty"`
|
||||
EnabledRoutes []string `protobuf:"bytes,18,rep,name=enabled_routes,json=enabledRoutes,proto3" json:"enabled_routes,omitempty"`
|
||||
AdvertisedRoutes []string `protobuf:"bytes,19,rep,name=advertised_routes,json=advertisedRoutes,proto3" json:"advertised_routes,omitempty"`
|
||||
ClientConnectivity *ClientConnectivity `protobuf:"bytes,20,opt,name=client_connectivity,json=clientConnectivity,proto3" json:"client_connectivity,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) Reset() {
|
||||
*x = GetDeviceResponse{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetDeviceResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetDeviceResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetDeviceResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetDeviceResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetAddresses() []string {
|
||||
if x != nil {
|
||||
return x.Addresses
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetUser() string {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetHostname() string {
|
||||
if x != nil {
|
||||
return x.Hostname
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetClientVersion() string {
|
||||
if x != nil {
|
||||
return x.ClientVersion
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetUpdateAvailable() bool {
|
||||
if x != nil {
|
||||
return x.UpdateAvailable
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetOs() string {
|
||||
if x != nil {
|
||||
return x.Os
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetCreated() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.Created
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetLastSeen() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.LastSeen
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetKeyExpiryDisabled() bool {
|
||||
if x != nil {
|
||||
return x.KeyExpiryDisabled
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetExpires() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.Expires
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetAuthorized() bool {
|
||||
if x != nil {
|
||||
return x.Authorized
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetIsExternal() bool {
|
||||
if x != nil {
|
||||
return x.IsExternal
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetMachineKey() string {
|
||||
if x != nil {
|
||||
return x.MachineKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetNodeKey() string {
|
||||
if x != nil {
|
||||
return x.NodeKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetBlocksIncomingConnections() bool {
|
||||
if x != nil {
|
||||
return x.BlocksIncomingConnections
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetEnabledRoutes() []string {
|
||||
if x != nil {
|
||||
return x.EnabledRoutes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetAdvertisedRoutes() []string {
|
||||
if x != nil {
|
||||
return x.AdvertisedRoutes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetDeviceResponse) GetClientConnectivity() *ClientConnectivity {
|
||||
if x != nil {
|
||||
return x.ClientConnectivity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeleteDeviceRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteDeviceRequest) Reset() {
|
||||
*x = DeleteDeviceRequest{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeleteDeviceRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeleteDeviceRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DeleteDeviceRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeleteDeviceRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DeleteDeviceRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *DeleteDeviceRequest) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type DeleteDeviceResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteDeviceResponse) Reset() {
|
||||
*x = DeleteDeviceResponse{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeleteDeviceResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeleteDeviceResponse) ProtoMessage() {}
|
||||
|
||||
func (x *DeleteDeviceResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeleteDeviceResponse.ProtoReflect.Descriptor instead.
|
||||
func (*DeleteDeviceResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
type GetDeviceRoutesRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetDeviceRoutesRequest) Reset() {
|
||||
*x = GetDeviceRoutesRequest{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetDeviceRoutesRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetDeviceRoutesRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetDeviceRoutesRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetDeviceRoutesRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetDeviceRoutesRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *GetDeviceRoutesRequest) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type GetDeviceRoutesResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
EnabledRoutes []string `protobuf:"bytes,1,rep,name=enabled_routes,json=enabledRoutes,proto3" json:"enabled_routes,omitempty"`
|
||||
AdvertisedRoutes []string `protobuf:"bytes,2,rep,name=advertised_routes,json=advertisedRoutes,proto3" json:"advertised_routes,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetDeviceRoutesResponse) Reset() {
|
||||
*x = GetDeviceRoutesResponse{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetDeviceRoutesResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetDeviceRoutesResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetDeviceRoutesResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetDeviceRoutesResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetDeviceRoutesResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *GetDeviceRoutesResponse) GetEnabledRoutes() []string {
|
||||
if x != nil {
|
||||
return x.EnabledRoutes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetDeviceRoutesResponse) GetAdvertisedRoutes() []string {
|
||||
if x != nil {
|
||||
return x.AdvertisedRoutes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type EnableDeviceRoutesRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Routes []string `protobuf:"bytes,2,rep,name=routes,proto3" json:"routes,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesRequest) Reset() {
|
||||
*x = EnableDeviceRoutesRequest{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*EnableDeviceRoutesRequest) ProtoMessage() {}
|
||||
|
||||
func (x *EnableDeviceRoutesRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use EnableDeviceRoutesRequest.ProtoReflect.Descriptor instead.
|
||||
func (*EnableDeviceRoutesRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesRequest) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesRequest) GetRoutes() []string {
|
||||
if x != nil {
|
||||
return x.Routes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type EnableDeviceRoutesResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
EnabledRoutes []string `protobuf:"bytes,1,rep,name=enabled_routes,json=enabledRoutes,proto3" json:"enabled_routes,omitempty"`
|
||||
AdvertisedRoutes []string `protobuf:"bytes,2,rep,name=advertised_routes,json=advertisedRoutes,proto3" json:"advertised_routes,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesResponse) Reset() {
|
||||
*x = EnableDeviceRoutesResponse{}
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*EnableDeviceRoutesResponse) ProtoMessage() {}
|
||||
|
||||
func (x *EnableDeviceRoutesResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_device_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use EnableDeviceRoutesResponse.ProtoReflect.Descriptor instead.
|
||||
func (*EnableDeviceRoutesResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_device_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesResponse) GetEnabledRoutes() []string {
|
||||
if x != nil {
|
||||
return x.EnabledRoutes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *EnableDeviceRoutesResponse) GetAdvertisedRoutes() []string {
|
||||
if x != nil {
|
||||
return x.AdvertisedRoutes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_headscale_v1_device_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_headscale_v1_device_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x19headscale/v1/device.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"F\n" +
|
||||
"\aLatency\x12\x1d\n" +
|
||||
"\n" +
|
||||
"latency_ms\x18\x01 \x01(\x02R\tlatencyMs\x12\x1c\n" +
|
||||
"\tpreferred\x18\x02 \x01(\bR\tpreferred\"\x91\x01\n" +
|
||||
"\x0eClientSupports\x12!\n" +
|
||||
"\fhair_pinning\x18\x01 \x01(\bR\vhairPinning\x12\x12\n" +
|
||||
"\x04ipv6\x18\x02 \x01(\bR\x04ipv6\x12\x10\n" +
|
||||
"\x03pcp\x18\x03 \x01(\bR\x03pcp\x12\x10\n" +
|
||||
"\x03pmp\x18\x04 \x01(\bR\x03pmp\x12\x10\n" +
|
||||
"\x03udp\x18\x05 \x01(\bR\x03udp\x12\x12\n" +
|
||||
"\x04upnp\x18\x06 \x01(\bR\x04upnp\"\xe3\x02\n" +
|
||||
"\x12ClientConnectivity\x12\x1c\n" +
|
||||
"\tendpoints\x18\x01 \x03(\tR\tendpoints\x12\x12\n" +
|
||||
"\x04derp\x18\x02 \x01(\tR\x04derp\x128\n" +
|
||||
"\x19mapping_varies_by_dest_ip\x18\x03 \x01(\bR\x15mappingVariesByDestIp\x12G\n" +
|
||||
"\alatency\x18\x04 \x03(\v2-.headscale.v1.ClientConnectivity.LatencyEntryR\alatency\x12E\n" +
|
||||
"\x0fclient_supports\x18\x05 \x01(\v2\x1c.headscale.v1.ClientSupportsR\x0eclientSupports\x1aQ\n" +
|
||||
"\fLatencyEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12+\n" +
|
||||
"\x05value\x18\x02 \x01(\v2\x15.headscale.v1.LatencyR\x05value:\x028\x01\"\"\n" +
|
||||
"\x10GetDeviceRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\"\xa0\x06\n" +
|
||||
"\x11GetDeviceResponse\x12\x1c\n" +
|
||||
"\taddresses\x18\x01 \x03(\tR\taddresses\x12\x0e\n" +
|
||||
"\x02id\x18\x02 \x01(\tR\x02id\x12\x12\n" +
|
||||
"\x04user\x18\x03 \x01(\tR\x04user\x12\x12\n" +
|
||||
"\x04name\x18\x04 \x01(\tR\x04name\x12\x1a\n" +
|
||||
"\bhostname\x18\x05 \x01(\tR\bhostname\x12%\n" +
|
||||
"\x0eclient_version\x18\x06 \x01(\tR\rclientVersion\x12)\n" +
|
||||
"\x10update_available\x18\a \x01(\bR\x0fupdateAvailable\x12\x0e\n" +
|
||||
"\x02os\x18\b \x01(\tR\x02os\x124\n" +
|
||||
"\acreated\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\acreated\x127\n" +
|
||||
"\tlast_seen\x18\n" +
|
||||
" \x01(\v2\x1a.google.protobuf.TimestampR\blastSeen\x12.\n" +
|
||||
"\x13key_expiry_disabled\x18\v \x01(\bR\x11keyExpiryDisabled\x124\n" +
|
||||
"\aexpires\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\aexpires\x12\x1e\n" +
|
||||
"\n" +
|
||||
"authorized\x18\r \x01(\bR\n" +
|
||||
"authorized\x12\x1f\n" +
|
||||
"\vis_external\x18\x0e \x01(\bR\n" +
|
||||
"isExternal\x12\x1f\n" +
|
||||
"\vmachine_key\x18\x0f \x01(\tR\n" +
|
||||
"machineKey\x12\x19\n" +
|
||||
"\bnode_key\x18\x10 \x01(\tR\anodeKey\x12>\n" +
|
||||
"\x1bblocks_incoming_connections\x18\x11 \x01(\bR\x19blocksIncomingConnections\x12%\n" +
|
||||
"\x0eenabled_routes\x18\x12 \x03(\tR\renabledRoutes\x12+\n" +
|
||||
"\x11advertised_routes\x18\x13 \x03(\tR\x10advertisedRoutes\x12Q\n" +
|
||||
"\x13client_connectivity\x18\x14 \x01(\v2 .headscale.v1.ClientConnectivityR\x12clientConnectivity\"%\n" +
|
||||
"\x13DeleteDeviceRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\"\x16\n" +
|
||||
"\x14DeleteDeviceResponse\"(\n" +
|
||||
"\x16GetDeviceRoutesRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\"m\n" +
|
||||
"\x17GetDeviceRoutesResponse\x12%\n" +
|
||||
"\x0eenabled_routes\x18\x01 \x03(\tR\renabledRoutes\x12+\n" +
|
||||
"\x11advertised_routes\x18\x02 \x03(\tR\x10advertisedRoutes\"C\n" +
|
||||
"\x19EnableDeviceRoutesRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" +
|
||||
"\x06routes\x18\x02 \x03(\tR\x06routes\"p\n" +
|
||||
"\x1aEnableDeviceRoutesResponse\x12%\n" +
|
||||
"\x0eenabled_routes\x18\x01 \x03(\tR\renabledRoutes\x12+\n" +
|
||||
"\x11advertised_routes\x18\x02 \x03(\tR\x10advertisedRoutesB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_headscale_v1_device_proto_rawDescOnce sync.Once
|
||||
file_headscale_v1_device_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_headscale_v1_device_proto_rawDescGZIP() []byte {
|
||||
file_headscale_v1_device_proto_rawDescOnce.Do(func() {
|
||||
file_headscale_v1_device_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_device_proto_rawDesc), len(file_headscale_v1_device_proto_rawDesc)))
|
||||
})
|
||||
return file_headscale_v1_device_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_headscale_v1_device_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||
var file_headscale_v1_device_proto_goTypes = []any{
|
||||
(*Latency)(nil), // 0: headscale.v1.Latency
|
||||
(*ClientSupports)(nil), // 1: headscale.v1.ClientSupports
|
||||
(*ClientConnectivity)(nil), // 2: headscale.v1.ClientConnectivity
|
||||
(*GetDeviceRequest)(nil), // 3: headscale.v1.GetDeviceRequest
|
||||
(*GetDeviceResponse)(nil), // 4: headscale.v1.GetDeviceResponse
|
||||
(*DeleteDeviceRequest)(nil), // 5: headscale.v1.DeleteDeviceRequest
|
||||
(*DeleteDeviceResponse)(nil), // 6: headscale.v1.DeleteDeviceResponse
|
||||
(*GetDeviceRoutesRequest)(nil), // 7: headscale.v1.GetDeviceRoutesRequest
|
||||
(*GetDeviceRoutesResponse)(nil), // 8: headscale.v1.GetDeviceRoutesResponse
|
||||
(*EnableDeviceRoutesRequest)(nil), // 9: headscale.v1.EnableDeviceRoutesRequest
|
||||
(*EnableDeviceRoutesResponse)(nil), // 10: headscale.v1.EnableDeviceRoutesResponse
|
||||
nil, // 11: headscale.v1.ClientConnectivity.LatencyEntry
|
||||
(*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp
|
||||
}
|
||||
var file_headscale_v1_device_proto_depIdxs = []int32{
|
||||
11, // 0: headscale.v1.ClientConnectivity.latency:type_name -> headscale.v1.ClientConnectivity.LatencyEntry
|
||||
1, // 1: headscale.v1.ClientConnectivity.client_supports:type_name -> headscale.v1.ClientSupports
|
||||
12, // 2: headscale.v1.GetDeviceResponse.created:type_name -> google.protobuf.Timestamp
|
||||
12, // 3: headscale.v1.GetDeviceResponse.last_seen:type_name -> google.protobuf.Timestamp
|
||||
12, // 4: headscale.v1.GetDeviceResponse.expires:type_name -> google.protobuf.Timestamp
|
||||
2, // 5: headscale.v1.GetDeviceResponse.client_connectivity:type_name -> headscale.v1.ClientConnectivity
|
||||
0, // 6: headscale.v1.ClientConnectivity.LatencyEntry.value:type_name -> headscale.v1.Latency
|
||||
7, // [7:7] is the sub-list for method output_type
|
||||
7, // [7:7] is the sub-list for method input_type
|
||||
7, // [7:7] is the sub-list for extension type_name
|
||||
7, // [7:7] is the sub-list for extension extendee
|
||||
0, // [0:7] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_device_proto_init() }
|
||||
func file_headscale_v1_device_proto_init() {
|
||||
if File_headscale_v1_device_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_device_proto_rawDesc), len(file_headscale_v1_device_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 12,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_headscale_v1_device_proto_goTypes,
|
||||
DependencyIndexes: file_headscale_v1_device_proto_depIdxs,
|
||||
MessageInfos: file_headscale_v1_device_proto_msgTypes,
|
||||
}.Build()
|
||||
File_headscale_v1_device_proto = out.File
|
||||
file_headscale_v1_device_proto_goTypes = nil
|
||||
file_headscale_v1_device_proto_depIdxs = nil
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/headscale.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
_ "google.golang.org/genproto/googleapis/api/annotations"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type HealthRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *HealthRequest) Reset() {
|
||||
*x = HealthRequest{}
|
||||
mi := &file_headscale_v1_headscale_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *HealthRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*HealthRequest) ProtoMessage() {}
|
||||
|
||||
func (x *HealthRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_headscale_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use HealthRequest.ProtoReflect.Descriptor instead.
|
||||
func (*HealthRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_headscale_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
type HealthResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
DatabaseConnectivity bool `protobuf:"varint,1,opt,name=database_connectivity,json=databaseConnectivity,proto3" json:"database_connectivity,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *HealthResponse) Reset() {
|
||||
*x = HealthResponse{}
|
||||
mi := &file_headscale_v1_headscale_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *HealthResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*HealthResponse) ProtoMessage() {}
|
||||
|
||||
func (x *HealthResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_headscale_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use HealthResponse.ProtoReflect.Descriptor instead.
|
||||
func (*HealthResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_headscale_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *HealthResponse) GetDatabaseConnectivity() bool {
|
||||
if x != nil {
|
||||
return x.DatabaseConnectivity
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var File_headscale_v1_headscale_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_headscale_v1_headscale_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x1cheadscale/v1/headscale.proto\x12\fheadscale.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17headscale/v1/user.proto\x1a\x1dheadscale/v1/preauthkey.proto\x1a\x17headscale/v1/node.proto\x1a\x19headscale/v1/apikey.proto\x1a\x17headscale/v1/auth.proto\x1a\x19headscale/v1/policy.proto\"\x0f\n" +
|
||||
"\rHealthRequest\"E\n" +
|
||||
"\x0eHealthResponse\x123\n" +
|
||||
"\x15database_connectivity\x18\x01 \x01(\bR\x14databaseConnectivity2\xe0\x1a\n" +
|
||||
"\x10HeadscaleService\x12h\n" +
|
||||
"\n" +
|
||||
"CreateUser\x12\x1f.headscale.v1.CreateUserRequest\x1a .headscale.v1.CreateUserResponse\"\x17\x82\xd3\xe4\x93\x02\x11:\x01*\"\f/api/v1/user\x12\x80\x01\n" +
|
||||
"\n" +
|
||||
"RenameUser\x12\x1f.headscale.v1.RenameUserRequest\x1a .headscale.v1.RenameUserResponse\"/\x82\xd3\xe4\x93\x02)\"'/api/v1/user/{old_id}/rename/{new_name}\x12j\n" +
|
||||
"\n" +
|
||||
"DeleteUser\x12\x1f.headscale.v1.DeleteUserRequest\x1a .headscale.v1.DeleteUserResponse\"\x19\x82\xd3\xe4\x93\x02\x13*\x11/api/v1/user/{id}\x12b\n" +
|
||||
"\tListUsers\x12\x1e.headscale.v1.ListUsersRequest\x1a\x1f.headscale.v1.ListUsersResponse\"\x14\x82\xd3\xe4\x93\x02\x0e\x12\f/api/v1/user\x12\x80\x01\n" +
|
||||
"\x10CreatePreAuthKey\x12%.headscale.v1.CreatePreAuthKeyRequest\x1a&.headscale.v1.CreatePreAuthKeyResponse\"\x1d\x82\xd3\xe4\x93\x02\x17:\x01*\"\x12/api/v1/preauthkey\x12\x87\x01\n" +
|
||||
"\x10ExpirePreAuthKey\x12%.headscale.v1.ExpirePreAuthKeyRequest\x1a&.headscale.v1.ExpirePreAuthKeyResponse\"$\x82\xd3\xe4\x93\x02\x1e:\x01*\"\x19/api/v1/preauthkey/expire\x12}\n" +
|
||||
"\x10DeletePreAuthKey\x12%.headscale.v1.DeletePreAuthKeyRequest\x1a&.headscale.v1.DeletePreAuthKeyResponse\"\x1a\x82\xd3\xe4\x93\x02\x14*\x12/api/v1/preauthkey\x12z\n" +
|
||||
"\x0fListPreAuthKeys\x12$.headscale.v1.ListPreAuthKeysRequest\x1a%.headscale.v1.ListPreAuthKeysResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/api/v1/preauthkey\x12}\n" +
|
||||
"\x0fDebugCreateNode\x12$.headscale.v1.DebugCreateNodeRequest\x1a%.headscale.v1.DebugCreateNodeResponse\"\x1d\x82\xd3\xe4\x93\x02\x17:\x01*\"\x12/api/v1/debug/node\x12f\n" +
|
||||
"\aGetNode\x12\x1c.headscale.v1.GetNodeRequest\x1a\x1d.headscale.v1.GetNodeResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/api/v1/node/{node_id}\x12n\n" +
|
||||
"\aSetTags\x12\x1c.headscale.v1.SetTagsRequest\x1a\x1d.headscale.v1.SetTagsResponse\"&\x82\xd3\xe4\x93\x02 :\x01*\"\x1b/api/v1/node/{node_id}/tags\x12\x96\x01\n" +
|
||||
"\x11SetApprovedRoutes\x12&.headscale.v1.SetApprovedRoutesRequest\x1a'.headscale.v1.SetApprovedRoutesResponse\"0\x82\xd3\xe4\x93\x02*:\x01*\"%/api/v1/node/{node_id}/approve_routes\x12t\n" +
|
||||
"\fRegisterNode\x12!.headscale.v1.RegisterNodeRequest\x1a\".headscale.v1.RegisterNodeResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\"\x15/api/v1/node/register\x12o\n" +
|
||||
"\n" +
|
||||
"DeleteNode\x12\x1f.headscale.v1.DeleteNodeRequest\x1a .headscale.v1.DeleteNodeResponse\"\x1e\x82\xd3\xe4\x93\x02\x18*\x16/api/v1/node/{node_id}\x12v\n" +
|
||||
"\n" +
|
||||
"ExpireNode\x12\x1f.headscale.v1.ExpireNodeRequest\x1a .headscale.v1.ExpireNodeResponse\"%\x82\xd3\xe4\x93\x02\x1f\"\x1d/api/v1/node/{node_id}/expire\x12\x81\x01\n" +
|
||||
"\n" +
|
||||
"RenameNode\x12\x1f.headscale.v1.RenameNodeRequest\x1a .headscale.v1.RenameNodeResponse\"0\x82\xd3\xe4\x93\x02*\"(/api/v1/node/{node_id}/rename/{new_name}\x12b\n" +
|
||||
"\tListNodes\x12\x1e.headscale.v1.ListNodesRequest\x1a\x1f.headscale.v1.ListNodesResponse\"\x14\x82\xd3\xe4\x93\x02\x0e\x12\f/api/v1/node\x12\x80\x01\n" +
|
||||
"\x0fBackfillNodeIPs\x12$.headscale.v1.BackfillNodeIPsRequest\x1a%.headscale.v1.BackfillNodeIPsResponse\" \x82\xd3\xe4\x93\x02\x1a\"\x18/api/v1/node/backfillips\x12w\n" +
|
||||
"\fAuthRegister\x12!.headscale.v1.AuthRegisterRequest\x1a\".headscale.v1.AuthRegisterResponse\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/api/v1/auth/register\x12s\n" +
|
||||
"\vAuthApprove\x12 .headscale.v1.AuthApproveRequest\x1a!.headscale.v1.AuthApproveResponse\"\x1f\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/api/v1/auth/approve\x12o\n" +
|
||||
"\n" +
|
||||
"AuthReject\x12\x1f.headscale.v1.AuthRejectRequest\x1a .headscale.v1.AuthRejectResponse\"\x1e\x82\xd3\xe4\x93\x02\x18:\x01*\"\x13/api/v1/auth/reject\x12p\n" +
|
||||
"\fCreateApiKey\x12!.headscale.v1.CreateApiKeyRequest\x1a\".headscale.v1.CreateApiKeyResponse\"\x19\x82\xd3\xe4\x93\x02\x13:\x01*\"\x0e/api/v1/apikey\x12w\n" +
|
||||
"\fExpireApiKey\x12!.headscale.v1.ExpireApiKeyRequest\x1a\".headscale.v1.ExpireApiKeyResponse\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/api/v1/apikey/expire\x12j\n" +
|
||||
"\vListApiKeys\x12 .headscale.v1.ListApiKeysRequest\x1a!.headscale.v1.ListApiKeysResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/api/v1/apikey\x12v\n" +
|
||||
"\fDeleteApiKey\x12!.headscale.v1.DeleteApiKeyRequest\x1a\".headscale.v1.DeleteApiKeyResponse\"\x1f\x82\xd3\xe4\x93\x02\x19*\x17/api/v1/apikey/{prefix}\x12d\n" +
|
||||
"\tGetPolicy\x12\x1e.headscale.v1.GetPolicyRequest\x1a\x1f.headscale.v1.GetPolicyResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/api/v1/policy\x12g\n" +
|
||||
"\tSetPolicy\x12\x1e.headscale.v1.SetPolicyRequest\x1a\x1f.headscale.v1.SetPolicyResponse\"\x19\x82\xd3\xe4\x93\x02\x13:\x01*\x1a\x0e/api/v1/policy\x12s\n" +
|
||||
"\vCheckPolicy\x12 .headscale.v1.CheckPolicyRequest\x1a!.headscale.v1.CheckPolicyResponse\"\x1f\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/api/v1/policy/check\x12[\n" +
|
||||
"\x06Health\x12\x1b.headscale.v1.HealthRequest\x1a\x1c.headscale.v1.HealthResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/api/v1/healthB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_headscale_v1_headscale_proto_rawDescOnce sync.Once
|
||||
file_headscale_v1_headscale_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_headscale_v1_headscale_proto_rawDescGZIP() []byte {
|
||||
file_headscale_v1_headscale_proto_rawDescOnce.Do(func() {
|
||||
file_headscale_v1_headscale_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_headscale_proto_rawDesc), len(file_headscale_v1_headscale_proto_rawDesc)))
|
||||
})
|
||||
return file_headscale_v1_headscale_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_headscale_v1_headscale_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_headscale_v1_headscale_proto_goTypes = []any{
|
||||
(*HealthRequest)(nil), // 0: headscale.v1.HealthRequest
|
||||
(*HealthResponse)(nil), // 1: headscale.v1.HealthResponse
|
||||
(*CreateUserRequest)(nil), // 2: headscale.v1.CreateUserRequest
|
||||
(*RenameUserRequest)(nil), // 3: headscale.v1.RenameUserRequest
|
||||
(*DeleteUserRequest)(nil), // 4: headscale.v1.DeleteUserRequest
|
||||
(*ListUsersRequest)(nil), // 5: headscale.v1.ListUsersRequest
|
||||
(*CreatePreAuthKeyRequest)(nil), // 6: headscale.v1.CreatePreAuthKeyRequest
|
||||
(*ExpirePreAuthKeyRequest)(nil), // 7: headscale.v1.ExpirePreAuthKeyRequest
|
||||
(*DeletePreAuthKeyRequest)(nil), // 8: headscale.v1.DeletePreAuthKeyRequest
|
||||
(*ListPreAuthKeysRequest)(nil), // 9: headscale.v1.ListPreAuthKeysRequest
|
||||
(*DebugCreateNodeRequest)(nil), // 10: headscale.v1.DebugCreateNodeRequest
|
||||
(*GetNodeRequest)(nil), // 11: headscale.v1.GetNodeRequest
|
||||
(*SetTagsRequest)(nil), // 12: headscale.v1.SetTagsRequest
|
||||
(*SetApprovedRoutesRequest)(nil), // 13: headscale.v1.SetApprovedRoutesRequest
|
||||
(*RegisterNodeRequest)(nil), // 14: headscale.v1.RegisterNodeRequest
|
||||
(*DeleteNodeRequest)(nil), // 15: headscale.v1.DeleteNodeRequest
|
||||
(*ExpireNodeRequest)(nil), // 16: headscale.v1.ExpireNodeRequest
|
||||
(*RenameNodeRequest)(nil), // 17: headscale.v1.RenameNodeRequest
|
||||
(*ListNodesRequest)(nil), // 18: headscale.v1.ListNodesRequest
|
||||
(*BackfillNodeIPsRequest)(nil), // 19: headscale.v1.BackfillNodeIPsRequest
|
||||
(*AuthRegisterRequest)(nil), // 20: headscale.v1.AuthRegisterRequest
|
||||
(*AuthApproveRequest)(nil), // 21: headscale.v1.AuthApproveRequest
|
||||
(*AuthRejectRequest)(nil), // 22: headscale.v1.AuthRejectRequest
|
||||
(*CreateApiKeyRequest)(nil), // 23: headscale.v1.CreateApiKeyRequest
|
||||
(*ExpireApiKeyRequest)(nil), // 24: headscale.v1.ExpireApiKeyRequest
|
||||
(*ListApiKeysRequest)(nil), // 25: headscale.v1.ListApiKeysRequest
|
||||
(*DeleteApiKeyRequest)(nil), // 26: headscale.v1.DeleteApiKeyRequest
|
||||
(*GetPolicyRequest)(nil), // 27: headscale.v1.GetPolicyRequest
|
||||
(*SetPolicyRequest)(nil), // 28: headscale.v1.SetPolicyRequest
|
||||
(*CheckPolicyRequest)(nil), // 29: headscale.v1.CheckPolicyRequest
|
||||
(*CreateUserResponse)(nil), // 30: headscale.v1.CreateUserResponse
|
||||
(*RenameUserResponse)(nil), // 31: headscale.v1.RenameUserResponse
|
||||
(*DeleteUserResponse)(nil), // 32: headscale.v1.DeleteUserResponse
|
||||
(*ListUsersResponse)(nil), // 33: headscale.v1.ListUsersResponse
|
||||
(*CreatePreAuthKeyResponse)(nil), // 34: headscale.v1.CreatePreAuthKeyResponse
|
||||
(*ExpirePreAuthKeyResponse)(nil), // 35: headscale.v1.ExpirePreAuthKeyResponse
|
||||
(*DeletePreAuthKeyResponse)(nil), // 36: headscale.v1.DeletePreAuthKeyResponse
|
||||
(*ListPreAuthKeysResponse)(nil), // 37: headscale.v1.ListPreAuthKeysResponse
|
||||
(*DebugCreateNodeResponse)(nil), // 38: headscale.v1.DebugCreateNodeResponse
|
||||
(*GetNodeResponse)(nil), // 39: headscale.v1.GetNodeResponse
|
||||
(*SetTagsResponse)(nil), // 40: headscale.v1.SetTagsResponse
|
||||
(*SetApprovedRoutesResponse)(nil), // 41: headscale.v1.SetApprovedRoutesResponse
|
||||
(*RegisterNodeResponse)(nil), // 42: headscale.v1.RegisterNodeResponse
|
||||
(*DeleteNodeResponse)(nil), // 43: headscale.v1.DeleteNodeResponse
|
||||
(*ExpireNodeResponse)(nil), // 44: headscale.v1.ExpireNodeResponse
|
||||
(*RenameNodeResponse)(nil), // 45: headscale.v1.RenameNodeResponse
|
||||
(*ListNodesResponse)(nil), // 46: headscale.v1.ListNodesResponse
|
||||
(*BackfillNodeIPsResponse)(nil), // 47: headscale.v1.BackfillNodeIPsResponse
|
||||
(*AuthRegisterResponse)(nil), // 48: headscale.v1.AuthRegisterResponse
|
||||
(*AuthApproveResponse)(nil), // 49: headscale.v1.AuthApproveResponse
|
||||
(*AuthRejectResponse)(nil), // 50: headscale.v1.AuthRejectResponse
|
||||
(*CreateApiKeyResponse)(nil), // 51: headscale.v1.CreateApiKeyResponse
|
||||
(*ExpireApiKeyResponse)(nil), // 52: headscale.v1.ExpireApiKeyResponse
|
||||
(*ListApiKeysResponse)(nil), // 53: headscale.v1.ListApiKeysResponse
|
||||
(*DeleteApiKeyResponse)(nil), // 54: headscale.v1.DeleteApiKeyResponse
|
||||
(*GetPolicyResponse)(nil), // 55: headscale.v1.GetPolicyResponse
|
||||
(*SetPolicyResponse)(nil), // 56: headscale.v1.SetPolicyResponse
|
||||
(*CheckPolicyResponse)(nil), // 57: headscale.v1.CheckPolicyResponse
|
||||
}
|
||||
var file_headscale_v1_headscale_proto_depIdxs = []int32{
|
||||
2, // 0: headscale.v1.HeadscaleService.CreateUser:input_type -> headscale.v1.CreateUserRequest
|
||||
3, // 1: headscale.v1.HeadscaleService.RenameUser:input_type -> headscale.v1.RenameUserRequest
|
||||
4, // 2: headscale.v1.HeadscaleService.DeleteUser:input_type -> headscale.v1.DeleteUserRequest
|
||||
5, // 3: headscale.v1.HeadscaleService.ListUsers:input_type -> headscale.v1.ListUsersRequest
|
||||
6, // 4: headscale.v1.HeadscaleService.CreatePreAuthKey:input_type -> headscale.v1.CreatePreAuthKeyRequest
|
||||
7, // 5: headscale.v1.HeadscaleService.ExpirePreAuthKey:input_type -> headscale.v1.ExpirePreAuthKeyRequest
|
||||
8, // 6: headscale.v1.HeadscaleService.DeletePreAuthKey:input_type -> headscale.v1.DeletePreAuthKeyRequest
|
||||
9, // 7: headscale.v1.HeadscaleService.ListPreAuthKeys:input_type -> headscale.v1.ListPreAuthKeysRequest
|
||||
10, // 8: headscale.v1.HeadscaleService.DebugCreateNode:input_type -> headscale.v1.DebugCreateNodeRequest
|
||||
11, // 9: headscale.v1.HeadscaleService.GetNode:input_type -> headscale.v1.GetNodeRequest
|
||||
12, // 10: headscale.v1.HeadscaleService.SetTags:input_type -> headscale.v1.SetTagsRequest
|
||||
13, // 11: headscale.v1.HeadscaleService.SetApprovedRoutes:input_type -> headscale.v1.SetApprovedRoutesRequest
|
||||
14, // 12: headscale.v1.HeadscaleService.RegisterNode:input_type -> headscale.v1.RegisterNodeRequest
|
||||
15, // 13: headscale.v1.HeadscaleService.DeleteNode:input_type -> headscale.v1.DeleteNodeRequest
|
||||
16, // 14: headscale.v1.HeadscaleService.ExpireNode:input_type -> headscale.v1.ExpireNodeRequest
|
||||
17, // 15: headscale.v1.HeadscaleService.RenameNode:input_type -> headscale.v1.RenameNodeRequest
|
||||
18, // 16: headscale.v1.HeadscaleService.ListNodes:input_type -> headscale.v1.ListNodesRequest
|
||||
19, // 17: headscale.v1.HeadscaleService.BackfillNodeIPs:input_type -> headscale.v1.BackfillNodeIPsRequest
|
||||
20, // 18: headscale.v1.HeadscaleService.AuthRegister:input_type -> headscale.v1.AuthRegisterRequest
|
||||
21, // 19: headscale.v1.HeadscaleService.AuthApprove:input_type -> headscale.v1.AuthApproveRequest
|
||||
22, // 20: headscale.v1.HeadscaleService.AuthReject:input_type -> headscale.v1.AuthRejectRequest
|
||||
23, // 21: headscale.v1.HeadscaleService.CreateApiKey:input_type -> headscale.v1.CreateApiKeyRequest
|
||||
24, // 22: headscale.v1.HeadscaleService.ExpireApiKey:input_type -> headscale.v1.ExpireApiKeyRequest
|
||||
25, // 23: headscale.v1.HeadscaleService.ListApiKeys:input_type -> headscale.v1.ListApiKeysRequest
|
||||
26, // 24: headscale.v1.HeadscaleService.DeleteApiKey:input_type -> headscale.v1.DeleteApiKeyRequest
|
||||
27, // 25: headscale.v1.HeadscaleService.GetPolicy:input_type -> headscale.v1.GetPolicyRequest
|
||||
28, // 26: headscale.v1.HeadscaleService.SetPolicy:input_type -> headscale.v1.SetPolicyRequest
|
||||
29, // 27: headscale.v1.HeadscaleService.CheckPolicy:input_type -> headscale.v1.CheckPolicyRequest
|
||||
0, // 28: headscale.v1.HeadscaleService.Health:input_type -> headscale.v1.HealthRequest
|
||||
30, // 29: headscale.v1.HeadscaleService.CreateUser:output_type -> headscale.v1.CreateUserResponse
|
||||
31, // 30: headscale.v1.HeadscaleService.RenameUser:output_type -> headscale.v1.RenameUserResponse
|
||||
32, // 31: headscale.v1.HeadscaleService.DeleteUser:output_type -> headscale.v1.DeleteUserResponse
|
||||
33, // 32: headscale.v1.HeadscaleService.ListUsers:output_type -> headscale.v1.ListUsersResponse
|
||||
34, // 33: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse
|
||||
35, // 34: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse
|
||||
36, // 35: headscale.v1.HeadscaleService.DeletePreAuthKey:output_type -> headscale.v1.DeletePreAuthKeyResponse
|
||||
37, // 36: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse
|
||||
38, // 37: headscale.v1.HeadscaleService.DebugCreateNode:output_type -> headscale.v1.DebugCreateNodeResponse
|
||||
39, // 38: headscale.v1.HeadscaleService.GetNode:output_type -> headscale.v1.GetNodeResponse
|
||||
40, // 39: headscale.v1.HeadscaleService.SetTags:output_type -> headscale.v1.SetTagsResponse
|
||||
41, // 40: headscale.v1.HeadscaleService.SetApprovedRoutes:output_type -> headscale.v1.SetApprovedRoutesResponse
|
||||
42, // 41: headscale.v1.HeadscaleService.RegisterNode:output_type -> headscale.v1.RegisterNodeResponse
|
||||
43, // 42: headscale.v1.HeadscaleService.DeleteNode:output_type -> headscale.v1.DeleteNodeResponse
|
||||
44, // 43: headscale.v1.HeadscaleService.ExpireNode:output_type -> headscale.v1.ExpireNodeResponse
|
||||
45, // 44: headscale.v1.HeadscaleService.RenameNode:output_type -> headscale.v1.RenameNodeResponse
|
||||
46, // 45: headscale.v1.HeadscaleService.ListNodes:output_type -> headscale.v1.ListNodesResponse
|
||||
47, // 46: headscale.v1.HeadscaleService.BackfillNodeIPs:output_type -> headscale.v1.BackfillNodeIPsResponse
|
||||
48, // 47: headscale.v1.HeadscaleService.AuthRegister:output_type -> headscale.v1.AuthRegisterResponse
|
||||
49, // 48: headscale.v1.HeadscaleService.AuthApprove:output_type -> headscale.v1.AuthApproveResponse
|
||||
50, // 49: headscale.v1.HeadscaleService.AuthReject:output_type -> headscale.v1.AuthRejectResponse
|
||||
51, // 50: headscale.v1.HeadscaleService.CreateApiKey:output_type -> headscale.v1.CreateApiKeyResponse
|
||||
52, // 51: headscale.v1.HeadscaleService.ExpireApiKey:output_type -> headscale.v1.ExpireApiKeyResponse
|
||||
53, // 52: headscale.v1.HeadscaleService.ListApiKeys:output_type -> headscale.v1.ListApiKeysResponse
|
||||
54, // 53: headscale.v1.HeadscaleService.DeleteApiKey:output_type -> headscale.v1.DeleteApiKeyResponse
|
||||
55, // 54: headscale.v1.HeadscaleService.GetPolicy:output_type -> headscale.v1.GetPolicyResponse
|
||||
56, // 55: headscale.v1.HeadscaleService.SetPolicy:output_type -> headscale.v1.SetPolicyResponse
|
||||
57, // 56: headscale.v1.HeadscaleService.CheckPolicy:output_type -> headscale.v1.CheckPolicyResponse
|
||||
1, // 57: headscale.v1.HeadscaleService.Health:output_type -> headscale.v1.HealthResponse
|
||||
29, // [29:58] is the sub-list for method output_type
|
||||
0, // [0:29] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_headscale_proto_init() }
|
||||
func file_headscale_v1_headscale_proto_init() {
|
||||
if File_headscale_v1_headscale_proto != nil {
|
||||
return
|
||||
}
|
||||
file_headscale_v1_user_proto_init()
|
||||
file_headscale_v1_preauthkey_proto_init()
|
||||
file_headscale_v1_node_proto_init()
|
||||
file_headscale_v1_apikey_proto_init()
|
||||
file_headscale_v1_auth_proto_init()
|
||||
file_headscale_v1_policy_proto_init()
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_headscale_proto_rawDesc), len(file_headscale_v1_headscale_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_headscale_v1_headscale_proto_goTypes,
|
||||
DependencyIndexes: file_headscale_v1_headscale_proto_depIdxs,
|
||||
MessageInfos: file_headscale_v1_headscale_proto_msgTypes,
|
||||
}.Build()
|
||||
File_headscale_v1_headscale_proto = out.File
|
||||
file_headscale_v1_headscale_proto_goTypes = nil
|
||||
file_headscale_v1_headscale_proto_depIdxs = nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,363 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/policy.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type SetPolicyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SetPolicyRequest) Reset() {
|
||||
*x = SetPolicyRequest{}
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SetPolicyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SetPolicyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SetPolicyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SetPolicyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SetPolicyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *SetPolicyRequest) GetPolicy() string {
|
||||
if x != nil {
|
||||
return x.Policy
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type SetPolicyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"`
|
||||
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SetPolicyResponse) Reset() {
|
||||
*x = SetPolicyResponse{}
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SetPolicyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SetPolicyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SetPolicyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SetPolicyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*SetPolicyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *SetPolicyResponse) GetPolicy() string {
|
||||
if x != nil {
|
||||
return x.Policy
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SetPolicyResponse) GetUpdatedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.UpdatedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetPolicyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetPolicyRequest) Reset() {
|
||||
*x = GetPolicyRequest{}
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetPolicyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetPolicyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetPolicyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetPolicyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetPolicyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
type GetPolicyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"`
|
||||
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetPolicyResponse) Reset() {
|
||||
*x = GetPolicyResponse{}
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetPolicyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetPolicyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetPolicyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetPolicyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetPolicyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *GetPolicyResponse) GetPolicy() string {
|
||||
if x != nil {
|
||||
return x.Policy
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetPolicyResponse) GetUpdatedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.UpdatedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CheckPolicyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CheckPolicyRequest) Reset() {
|
||||
*x = CheckPolicyRequest{}
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CheckPolicyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CheckPolicyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CheckPolicyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CheckPolicyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CheckPolicyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *CheckPolicyRequest) GetPolicy() string {
|
||||
if x != nil {
|
||||
return x.Policy
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type CheckPolicyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CheckPolicyResponse) Reset() {
|
||||
*x = CheckPolicyResponse{}
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CheckPolicyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CheckPolicyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CheckPolicyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_policy_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CheckPolicyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CheckPolicyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
var File_headscale_v1_policy_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_headscale_v1_policy_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x19headscale/v1/policy.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"*\n" +
|
||||
"\x10SetPolicyRequest\x12\x16\n" +
|
||||
"\x06policy\x18\x01 \x01(\tR\x06policy\"f\n" +
|
||||
"\x11SetPolicyResponse\x12\x16\n" +
|
||||
"\x06policy\x18\x01 \x01(\tR\x06policy\x129\n" +
|
||||
"\n" +
|
||||
"updated_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\x12\n" +
|
||||
"\x10GetPolicyRequest\"f\n" +
|
||||
"\x11GetPolicyResponse\x12\x16\n" +
|
||||
"\x06policy\x18\x01 \x01(\tR\x06policy\x129\n" +
|
||||
"\n" +
|
||||
"updated_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\",\n" +
|
||||
"\x12CheckPolicyRequest\x12\x16\n" +
|
||||
"\x06policy\x18\x01 \x01(\tR\x06policy\"\x15\n" +
|
||||
"\x13CheckPolicyResponseB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_headscale_v1_policy_proto_rawDescOnce sync.Once
|
||||
file_headscale_v1_policy_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_headscale_v1_policy_proto_rawDescGZIP() []byte {
|
||||
file_headscale_v1_policy_proto_rawDescOnce.Do(func() {
|
||||
file_headscale_v1_policy_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_policy_proto_rawDesc), len(file_headscale_v1_policy_proto_rawDesc)))
|
||||
})
|
||||
return file_headscale_v1_policy_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_headscale_v1_policy_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||
var file_headscale_v1_policy_proto_goTypes = []any{
|
||||
(*SetPolicyRequest)(nil), // 0: headscale.v1.SetPolicyRequest
|
||||
(*SetPolicyResponse)(nil), // 1: headscale.v1.SetPolicyResponse
|
||||
(*GetPolicyRequest)(nil), // 2: headscale.v1.GetPolicyRequest
|
||||
(*GetPolicyResponse)(nil), // 3: headscale.v1.GetPolicyResponse
|
||||
(*CheckPolicyRequest)(nil), // 4: headscale.v1.CheckPolicyRequest
|
||||
(*CheckPolicyResponse)(nil), // 5: headscale.v1.CheckPolicyResponse
|
||||
(*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp
|
||||
}
|
||||
var file_headscale_v1_policy_proto_depIdxs = []int32{
|
||||
6, // 0: headscale.v1.SetPolicyResponse.updated_at:type_name -> google.protobuf.Timestamp
|
||||
6, // 1: headscale.v1.GetPolicyResponse.updated_at:type_name -> google.protobuf.Timestamp
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_policy_proto_init() }
|
||||
func file_headscale_v1_policy_proto_init() {
|
||||
if File_headscale_v1_policy_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_policy_proto_rawDesc), len(file_headscale_v1_policy_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 6,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_headscale_v1_policy_proto_goTypes,
|
||||
DependencyIndexes: file_headscale_v1_policy_proto_depIdxs,
|
||||
MessageInfos: file_headscale_v1_policy_proto_msgTypes,
|
||||
}.Build()
|
||||
File_headscale_v1_policy_proto = out.File
|
||||
file_headscale_v1_policy_proto_goTypes = nil
|
||||
file_headscale_v1_policy_proto_depIdxs = nil
|
||||
}
|
||||
@@ -1,596 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/preauthkey.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type PreAuthKey struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Id uint64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"`
|
||||
Reusable bool `protobuf:"varint,4,opt,name=reusable,proto3" json:"reusable,omitempty"`
|
||||
Ephemeral bool `protobuf:"varint,5,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"`
|
||||
Used bool `protobuf:"varint,6,opt,name=used,proto3" json:"used,omitempty"`
|
||||
Expiration *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expiration,proto3" json:"expiration,omitempty"`
|
||||
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
AclTags []string `protobuf:"bytes,9,rep,name=acl_tags,json=aclTags,proto3" json:"acl_tags,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) Reset() {
|
||||
*x = PreAuthKey{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PreAuthKey) ProtoMessage() {}
|
||||
|
||||
func (x *PreAuthKey) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PreAuthKey.ProtoReflect.Descriptor instead.
|
||||
func (*PreAuthKey) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetUser() *User {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetKey() string {
|
||||
if x != nil {
|
||||
return x.Key
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetReusable() bool {
|
||||
if x != nil {
|
||||
return x.Reusable
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetEphemeral() bool {
|
||||
if x != nil {
|
||||
return x.Ephemeral
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetUsed() bool {
|
||||
if x != nil {
|
||||
return x.Used
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetExpiration() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.Expiration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetCreatedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.CreatedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *PreAuthKey) GetAclTags() []string {
|
||||
if x != nil {
|
||||
return x.AclTags
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CreatePreAuthKeyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
User uint64 `protobuf:"varint,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Reusable bool `protobuf:"varint,2,opt,name=reusable,proto3" json:"reusable,omitempty"`
|
||||
Ephemeral bool `protobuf:"varint,3,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"`
|
||||
Expiration *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expiration,proto3" json:"expiration,omitempty"`
|
||||
AclTags []string `protobuf:"bytes,5,rep,name=acl_tags,json=aclTags,proto3" json:"acl_tags,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) Reset() {
|
||||
*x = CreatePreAuthKeyRequest{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreatePreAuthKeyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreatePreAuthKeyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CreatePreAuthKeyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) GetUser() uint64 {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) GetReusable() bool {
|
||||
if x != nil {
|
||||
return x.Reusable
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) GetEphemeral() bool {
|
||||
if x != nil {
|
||||
return x.Ephemeral
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) GetExpiration() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.Expiration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyRequest) GetAclTags() []string {
|
||||
if x != nil {
|
||||
return x.AclTags
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CreatePreAuthKeyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
PreAuthKey *PreAuthKey `protobuf:"bytes,1,opt,name=pre_auth_key,json=preAuthKey,proto3" json:"pre_auth_key,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyResponse) Reset() {
|
||||
*x = CreatePreAuthKeyResponse{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreatePreAuthKeyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CreatePreAuthKeyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreatePreAuthKeyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CreatePreAuthKeyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *CreatePreAuthKeyResponse) GetPreAuthKey() *PreAuthKey {
|
||||
if x != nil {
|
||||
return x.PreAuthKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ExpirePreAuthKeyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ExpirePreAuthKeyRequest) Reset() {
|
||||
*x = ExpirePreAuthKeyRequest{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExpirePreAuthKeyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExpirePreAuthKeyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ExpirePreAuthKeyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExpirePreAuthKeyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ExpirePreAuthKeyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *ExpirePreAuthKeyRequest) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ExpirePreAuthKeyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ExpirePreAuthKeyResponse) Reset() {
|
||||
*x = ExpirePreAuthKeyResponse{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExpirePreAuthKeyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExpirePreAuthKeyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ExpirePreAuthKeyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExpirePreAuthKeyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ExpirePreAuthKeyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
type DeletePreAuthKeyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeletePreAuthKeyRequest) Reset() {
|
||||
*x = DeletePreAuthKeyRequest{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeletePreAuthKeyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeletePreAuthKeyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DeletePreAuthKeyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeletePreAuthKeyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DeletePreAuthKeyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *DeletePreAuthKeyRequest) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type DeletePreAuthKeyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeletePreAuthKeyResponse) Reset() {
|
||||
*x = DeletePreAuthKeyResponse{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeletePreAuthKeyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeletePreAuthKeyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *DeletePreAuthKeyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeletePreAuthKeyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*DeletePreAuthKeyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
type ListPreAuthKeysRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListPreAuthKeysRequest) Reset() {
|
||||
*x = ListPreAuthKeysRequest{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListPreAuthKeysRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListPreAuthKeysRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ListPreAuthKeysRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListPreAuthKeysRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ListPreAuthKeysRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
type ListPreAuthKeysResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
PreAuthKeys []*PreAuthKey `protobuf:"bytes,1,rep,name=pre_auth_keys,json=preAuthKeys,proto3" json:"pre_auth_keys,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListPreAuthKeysResponse) Reset() {
|
||||
*x = ListPreAuthKeysResponse{}
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListPreAuthKeysResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListPreAuthKeysResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ListPreAuthKeysResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_preauthkey_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListPreAuthKeysResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ListPreAuthKeysResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *ListPreAuthKeysResponse) GetPreAuthKeys() []*PreAuthKey {
|
||||
if x != nil {
|
||||
return x.PreAuthKeys
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_headscale_v1_preauthkey_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_headscale_v1_preauthkey_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x1dheadscale/v1/preauthkey.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17headscale/v1/user.proto\"\xb6\x02\n" +
|
||||
"\n" +
|
||||
"PreAuthKey\x12&\n" +
|
||||
"\x04user\x18\x01 \x01(\v2\x12.headscale.v1.UserR\x04user\x12\x0e\n" +
|
||||
"\x02id\x18\x02 \x01(\x04R\x02id\x12\x10\n" +
|
||||
"\x03key\x18\x03 \x01(\tR\x03key\x12\x1a\n" +
|
||||
"\breusable\x18\x04 \x01(\bR\breusable\x12\x1c\n" +
|
||||
"\tephemeral\x18\x05 \x01(\bR\tephemeral\x12\x12\n" +
|
||||
"\x04used\x18\x06 \x01(\bR\x04used\x12:\n" +
|
||||
"\n" +
|
||||
"expiration\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\n" +
|
||||
"expiration\x129\n" +
|
||||
"\n" +
|
||||
"created_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x19\n" +
|
||||
"\bacl_tags\x18\t \x03(\tR\aaclTags\"\xbe\x01\n" +
|
||||
"\x17CreatePreAuthKeyRequest\x12\x12\n" +
|
||||
"\x04user\x18\x01 \x01(\x04R\x04user\x12\x1a\n" +
|
||||
"\breusable\x18\x02 \x01(\bR\breusable\x12\x1c\n" +
|
||||
"\tephemeral\x18\x03 \x01(\bR\tephemeral\x12:\n" +
|
||||
"\n" +
|
||||
"expiration\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
|
||||
"expiration\x12\x19\n" +
|
||||
"\bacl_tags\x18\x05 \x03(\tR\aaclTags\"V\n" +
|
||||
"\x18CreatePreAuthKeyResponse\x12:\n" +
|
||||
"\fpre_auth_key\x18\x01 \x01(\v2\x18.headscale.v1.PreAuthKeyR\n" +
|
||||
"preAuthKey\")\n" +
|
||||
"\x17ExpirePreAuthKeyRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x04R\x02id\"\x1a\n" +
|
||||
"\x18ExpirePreAuthKeyResponse\")\n" +
|
||||
"\x17DeletePreAuthKeyRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x04R\x02id\"\x1a\n" +
|
||||
"\x18DeletePreAuthKeyResponse\"\x18\n" +
|
||||
"\x16ListPreAuthKeysRequest\"W\n" +
|
||||
"\x17ListPreAuthKeysResponse\x12<\n" +
|
||||
"\rpre_auth_keys\x18\x01 \x03(\v2\x18.headscale.v1.PreAuthKeyR\vpreAuthKeysB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_headscale_v1_preauthkey_proto_rawDescOnce sync.Once
|
||||
file_headscale_v1_preauthkey_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_headscale_v1_preauthkey_proto_rawDescGZIP() []byte {
|
||||
file_headscale_v1_preauthkey_proto_rawDescOnce.Do(func() {
|
||||
file_headscale_v1_preauthkey_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_preauthkey_proto_rawDesc), len(file_headscale_v1_preauthkey_proto_rawDesc)))
|
||||
})
|
||||
return file_headscale_v1_preauthkey_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_headscale_v1_preauthkey_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_headscale_v1_preauthkey_proto_goTypes = []any{
|
||||
(*PreAuthKey)(nil), // 0: headscale.v1.PreAuthKey
|
||||
(*CreatePreAuthKeyRequest)(nil), // 1: headscale.v1.CreatePreAuthKeyRequest
|
||||
(*CreatePreAuthKeyResponse)(nil), // 2: headscale.v1.CreatePreAuthKeyResponse
|
||||
(*ExpirePreAuthKeyRequest)(nil), // 3: headscale.v1.ExpirePreAuthKeyRequest
|
||||
(*ExpirePreAuthKeyResponse)(nil), // 4: headscale.v1.ExpirePreAuthKeyResponse
|
||||
(*DeletePreAuthKeyRequest)(nil), // 5: headscale.v1.DeletePreAuthKeyRequest
|
||||
(*DeletePreAuthKeyResponse)(nil), // 6: headscale.v1.DeletePreAuthKeyResponse
|
||||
(*ListPreAuthKeysRequest)(nil), // 7: headscale.v1.ListPreAuthKeysRequest
|
||||
(*ListPreAuthKeysResponse)(nil), // 8: headscale.v1.ListPreAuthKeysResponse
|
||||
(*User)(nil), // 9: headscale.v1.User
|
||||
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
|
||||
}
|
||||
var file_headscale_v1_preauthkey_proto_depIdxs = []int32{
|
||||
9, // 0: headscale.v1.PreAuthKey.user:type_name -> headscale.v1.User
|
||||
10, // 1: headscale.v1.PreAuthKey.expiration:type_name -> google.protobuf.Timestamp
|
||||
10, // 2: headscale.v1.PreAuthKey.created_at:type_name -> google.protobuf.Timestamp
|
||||
10, // 3: headscale.v1.CreatePreAuthKeyRequest.expiration:type_name -> google.protobuf.Timestamp
|
||||
0, // 4: headscale.v1.CreatePreAuthKeyResponse.pre_auth_key:type_name -> headscale.v1.PreAuthKey
|
||||
0, // 5: headscale.v1.ListPreAuthKeysResponse.pre_auth_keys:type_name -> headscale.v1.PreAuthKey
|
||||
6, // [6:6] is the sub-list for method output_type
|
||||
6, // [6:6] is the sub-list for method input_type
|
||||
6, // [6:6] is the sub-list for extension type_name
|
||||
6, // [6:6] is the sub-list for extension extendee
|
||||
0, // [0:6] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_preauthkey_proto_init() }
|
||||
func file_headscale_v1_preauthkey_proto_init() {
|
||||
if File_headscale_v1_preauthkey_proto != nil {
|
||||
return
|
||||
}
|
||||
file_headscale_v1_user_proto_init()
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_preauthkey_proto_rawDesc), len(file_headscale_v1_preauthkey_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 9,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_headscale_v1_preauthkey_proto_goTypes,
|
||||
DependencyIndexes: file_headscale_v1_preauthkey_proto_depIdxs,
|
||||
MessageInfos: file_headscale_v1_preauthkey_proto_msgTypes,
|
||||
}.Build()
|
||||
File_headscale_v1_preauthkey_proto = out.File
|
||||
file_headscale_v1_preauthkey_proto_goTypes = nil
|
||||
file_headscale_v1_preauthkey_proto_depIdxs = nil
|
||||
}
|
||||
@@ -1,615 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: headscale/v1/user.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type User struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
DisplayName string `protobuf:"bytes,4,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
|
||||
Email string `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"`
|
||||
ProviderId string `protobuf:"bytes,6,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"`
|
||||
Provider string `protobuf:"bytes,7,opt,name=provider,proto3" json:"provider,omitempty"`
|
||||
ProfilePicUrl string `protobuf:"bytes,8,opt,name=profile_pic_url,json=profilePicUrl,proto3" json:"profile_pic_url,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *User) Reset() {
|
||||
*x = User{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *User) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*User) ProtoMessage() {}
|
||||
|
||||
func (x *User) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use User.ProtoReflect.Descriptor instead.
|
||||
func (*User) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *User) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *User) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetCreatedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.CreatedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *User) GetDisplayName() string {
|
||||
if x != nil {
|
||||
return x.DisplayName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetEmail() string {
|
||||
if x != nil {
|
||||
return x.Email
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetProviderId() string {
|
||||
if x != nil {
|
||||
return x.ProviderId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetProvider() string {
|
||||
if x != nil {
|
||||
return x.Provider
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetProfilePicUrl() string {
|
||||
if x != nil {
|
||||
return x.ProfilePicUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type CreateUserRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
|
||||
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
|
||||
PictureUrl string `protobuf:"bytes,4,opt,name=picture_url,json=pictureUrl,proto3" json:"picture_url,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) Reset() {
|
||||
*x = CreateUserRequest{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreateUserRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CreateUserRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreateUserRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CreateUserRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) GetDisplayName() string {
|
||||
if x != nil {
|
||||
return x.DisplayName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) GetEmail() string {
|
||||
if x != nil {
|
||||
return x.Email
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CreateUserRequest) GetPictureUrl() string {
|
||||
if x != nil {
|
||||
return x.PictureUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type CreateUserResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreateUserResponse) Reset() {
|
||||
*x = CreateUserResponse{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreateUserResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreateUserResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CreateUserResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreateUserResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CreateUserResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *CreateUserResponse) GetUser() *User {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RenameUserRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
OldId uint64 `protobuf:"varint,1,opt,name=old_id,json=oldId,proto3" json:"old_id,omitempty"`
|
||||
NewName string `protobuf:"bytes,2,opt,name=new_name,json=newName,proto3" json:"new_name,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RenameUserRequest) Reset() {
|
||||
*x = RenameUserRequest{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RenameUserRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RenameUserRequest) ProtoMessage() {}
|
||||
|
||||
func (x *RenameUserRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RenameUserRequest.ProtoReflect.Descriptor instead.
|
||||
func (*RenameUserRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *RenameUserRequest) GetOldId() uint64 {
|
||||
if x != nil {
|
||||
return x.OldId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RenameUserRequest) GetNewName() string {
|
||||
if x != nil {
|
||||
return x.NewName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RenameUserResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RenameUserResponse) Reset() {
|
||||
*x = RenameUserResponse{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RenameUserResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RenameUserResponse) ProtoMessage() {}
|
||||
|
||||
func (x *RenameUserResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RenameUserResponse.ProtoReflect.Descriptor instead.
|
||||
func (*RenameUserResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *RenameUserResponse) GetUser() *User {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeleteUserRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteUserRequest) Reset() {
|
||||
*x = DeleteUserRequest{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeleteUserRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeleteUserRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DeleteUserRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeleteUserRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DeleteUserRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *DeleteUserRequest) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type DeleteUserResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeleteUserResponse) Reset() {
|
||||
*x = DeleteUserResponse{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeleteUserResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeleteUserResponse) ProtoMessage() {}
|
||||
|
||||
func (x *DeleteUserResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeleteUserResponse.ProtoReflect.Descriptor instead.
|
||||
func (*DeleteUserResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
type ListUsersRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListUsersRequest) Reset() {
|
||||
*x = ListUsersRequest{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListUsersRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListUsersRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ListUsersRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListUsersRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ListUsersRequest) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *ListUsersRequest) GetId() uint64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ListUsersRequest) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ListUsersRequest) GetEmail() string {
|
||||
if x != nil {
|
||||
return x.Email
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ListUsersResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListUsersResponse) Reset() {
|
||||
*x = ListUsersResponse{}
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListUsersResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListUsersResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ListUsersResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_headscale_v1_user_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListUsersResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ListUsersResponse) Descriptor() ([]byte, []int) {
|
||||
return file_headscale_v1_user_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *ListUsersResponse) GetUsers() []*User {
|
||||
if x != nil {
|
||||
return x.Users
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_headscale_v1_user_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_headscale_v1_user_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x17headscale/v1/user.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x83\x02\n" +
|
||||
"\x04User\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x04R\x02id\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x129\n" +
|
||||
"\n" +
|
||||
"created_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12!\n" +
|
||||
"\fdisplay_name\x18\x04 \x01(\tR\vdisplayName\x12\x14\n" +
|
||||
"\x05email\x18\x05 \x01(\tR\x05email\x12\x1f\n" +
|
||||
"\vprovider_id\x18\x06 \x01(\tR\n" +
|
||||
"providerId\x12\x1a\n" +
|
||||
"\bprovider\x18\a \x01(\tR\bprovider\x12&\n" +
|
||||
"\x0fprofile_pic_url\x18\b \x01(\tR\rprofilePicUrl\"\x81\x01\n" +
|
||||
"\x11CreateUserRequest\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12!\n" +
|
||||
"\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\x12\x14\n" +
|
||||
"\x05email\x18\x03 \x01(\tR\x05email\x12\x1f\n" +
|
||||
"\vpicture_url\x18\x04 \x01(\tR\n" +
|
||||
"pictureUrl\"<\n" +
|
||||
"\x12CreateUserResponse\x12&\n" +
|
||||
"\x04user\x18\x01 \x01(\v2\x12.headscale.v1.UserR\x04user\"E\n" +
|
||||
"\x11RenameUserRequest\x12\x15\n" +
|
||||
"\x06old_id\x18\x01 \x01(\x04R\x05oldId\x12\x19\n" +
|
||||
"\bnew_name\x18\x02 \x01(\tR\anewName\"<\n" +
|
||||
"\x12RenameUserResponse\x12&\n" +
|
||||
"\x04user\x18\x01 \x01(\v2\x12.headscale.v1.UserR\x04user\"#\n" +
|
||||
"\x11DeleteUserRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x04R\x02id\"\x14\n" +
|
||||
"\x12DeleteUserResponse\"L\n" +
|
||||
"\x10ListUsersRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x04R\x02id\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" +
|
||||
"\x05email\x18\x03 \x01(\tR\x05email\"=\n" +
|
||||
"\x11ListUsersResponse\x12(\n" +
|
||||
"\x05users\x18\x01 \x03(\v2\x12.headscale.v1.UserR\x05usersB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_headscale_v1_user_proto_rawDescOnce sync.Once
|
||||
file_headscale_v1_user_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_headscale_v1_user_proto_rawDescGZIP() []byte {
|
||||
file_headscale_v1_user_proto_rawDescOnce.Do(func() {
|
||||
file_headscale_v1_user_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_user_proto_rawDesc), len(file_headscale_v1_user_proto_rawDesc)))
|
||||
})
|
||||
return file_headscale_v1_user_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_headscale_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_headscale_v1_user_proto_goTypes = []any{
|
||||
(*User)(nil), // 0: headscale.v1.User
|
||||
(*CreateUserRequest)(nil), // 1: headscale.v1.CreateUserRequest
|
||||
(*CreateUserResponse)(nil), // 2: headscale.v1.CreateUserResponse
|
||||
(*RenameUserRequest)(nil), // 3: headscale.v1.RenameUserRequest
|
||||
(*RenameUserResponse)(nil), // 4: headscale.v1.RenameUserResponse
|
||||
(*DeleteUserRequest)(nil), // 5: headscale.v1.DeleteUserRequest
|
||||
(*DeleteUserResponse)(nil), // 6: headscale.v1.DeleteUserResponse
|
||||
(*ListUsersRequest)(nil), // 7: headscale.v1.ListUsersRequest
|
||||
(*ListUsersResponse)(nil), // 8: headscale.v1.ListUsersResponse
|
||||
(*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp
|
||||
}
|
||||
var file_headscale_v1_user_proto_depIdxs = []int32{
|
||||
9, // 0: headscale.v1.User.created_at:type_name -> google.protobuf.Timestamp
|
||||
0, // 1: headscale.v1.CreateUserResponse.user:type_name -> headscale.v1.User
|
||||
0, // 2: headscale.v1.RenameUserResponse.user:type_name -> headscale.v1.User
|
||||
0, // 3: headscale.v1.ListUsersResponse.users:type_name -> headscale.v1.User
|
||||
4, // [4:4] is the sub-list for method output_type
|
||||
4, // [4:4] is the sub-list for method input_type
|
||||
4, // [4:4] is the sub-list for extension type_name
|
||||
4, // [4:4] is the sub-list for extension extendee
|
||||
0, // [0:4] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_headscale_v1_user_proto_init() }
|
||||
func file_headscale_v1_user_proto_init() {
|
||||
if File_headscale_v1_user_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_user_proto_rawDesc), len(file_headscale_v1_user_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 9,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_headscale_v1_user_proto_goTypes,
|
||||
DependencyIndexes: file_headscale_v1_user_proto_depIdxs,
|
||||
MessageInfos: file_headscale_v1_user_proto_msgTypes,
|
||||
}.Build()
|
||||
File_headscale_v1_user_proto = out.File
|
||||
file_headscale_v1_user_proto_goTypes = nil
|
||||
file_headscale_v1_user_proto_depIdxs = nil
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "headscale/v1/apikey.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "headscale/v1/auth.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "headscale/v1/device.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "headscale/v1/node.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "headscale/v1/policy.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "headscale/v1/preauthkey.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "headscale/v1/user.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,16 +16,17 @@ require (
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-chi/chi/v5 v5.3.0
|
||||
github.com/go-chi/metrics v0.1.1
|
||||
github.com/go-faster/errors v0.7.1
|
||||
github.com/go-faster/jx v1.2.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6
|
||||
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6
|
||||
github.com/gofrs/uuid/v5 v5.4.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/jagottsicher/termcolor v1.0.2
|
||||
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25
|
||||
github.com/ogen-go/ogen v1.22.0
|
||||
github.com/ory/dockertest/v3 v3.12.0
|
||||
github.com/philip-bui/grpc-zerolog v1.0.1
|
||||
github.com/pkg/profile v1.7.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/common v0.68.1
|
||||
@@ -42,15 +43,15 @@ require (
|
||||
github.com/tailscale/squibble v0.0.0-20260411062017-141f5d618bc4
|
||||
github.com/tailscale/tailsql v0.0.0-20260522170732-77aec5aabc76
|
||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
||||
go.opentelemetry.io/otel v1.44.0
|
||||
go.opentelemetry.io/otel/metric v1.44.0
|
||||
go.opentelemetry.io/otel/trace v1.44.0
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/exp v0.0.0-20260603202125-055de637280b
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.21.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad
|
||||
google.golang.org/grpc v1.81.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
@@ -134,16 +135,20 @@ require (
|
||||
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.12.0 // indirect
|
||||
github.com/docker/cli v29.5.3+incompatible // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/felixge/fgprof v0.9.5 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fogleman/gg v1.3.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/gaissmai/bart v0.26.1 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-faster/yaml v0.4.6 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
@@ -154,7 +159,6 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-github v17.0.0+incompatible // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
@@ -163,6 +167,7 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gookit/color v1.6.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/hashicorp/go-version v1.9.0 // indirect
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||
github.com/huin/goupnp v1.3.0 // indirect
|
||||
@@ -209,6 +214,8 @@ require (
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/safchain/ethtool v0.7.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
@@ -227,32 +234,37 @@ require (
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yuin/goldmark v1.8.2 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.28.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20260603202125-055de637280b // indirect
|
||||
golang.org/x/image v0.41.0 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/term v0.44.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
golang.org/x/tools v0.46.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/grpc v1.81.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/client-go v0.34.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0 // indirect
|
||||
)
|
||||
|
||||
tool (
|
||||
github.com/ogen-go/ogen/cmd/ogen
|
||||
golang.org/x/tools/cmd/stress
|
||||
golang.org/x/tools/cmd/stringer
|
||||
tailscale.com/cmd/tailscale
|
||||
|
||||
@@ -146,6 +146,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs=
|
||||
github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
@@ -156,6 +158,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
|
||||
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
|
||||
@@ -171,6 +175,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
|
||||
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
@@ -181,6 +187,12 @@ github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
|
||||
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
github.com/go-chi/metrics v0.1.1 h1:CXhbnkAVVjb0k73EBRQ6Z2YdWFnbXZgNtg1Mboguibk=
|
||||
github.com/go-chi/metrics v0.1.1/go.mod h1:mcGTM1pPalP7WCtb+akNYFO/lwNwBBLCuedepqjoPn4=
|
||||
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
||||
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||
github.com/go-faster/jx v1.2.0 h1:T2YHJPrFaYu21fJtUxC9GzmluKu8rVIFDwwGBKTDseI=
|
||||
github.com/go-faster/jx v1.2.0/go.mod h1:UWLOVDmMG597a5tBFPLIWJdUxz5/2emOpfsj9Neg0PE=
|
||||
github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
|
||||
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6 h1:VtX+l1Stj2v5RGubVQk0LS/8EPGXR+ldcOyCmlmKoyg=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6/go.mod h1:PZpedQc4tWaxn6kvXicwhinh3L0seLpMc5ReKRX5id4=
|
||||
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
|
||||
@@ -217,8 +229,6 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@@ -355,6 +365,8 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk=
|
||||
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw=
|
||||
github.com/ogen-go/ogen v1.22.0 h1:7wU+jcIKg/JBAhM95909ULLdAkGr43KQOuvNpJ7Mxb4=
|
||||
github.com/ogen-go/ogen v1.22.0/go.mod h1:7BOh9a51QiPCC92RMrj1LlkLjejhBAyPhR+oMc6lR9g=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
@@ -371,8 +383,6 @@ github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyX
|
||||
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/philip-bui/grpc-zerolog v1.0.1 h1:EMacvLRUd2O1K0eWod27ZP5CY1iTNkhBDLSN+Q4JEvA=
|
||||
github.com/philip-bui/grpc-zerolog v1.0.1/go.mod h1:qXbiq/2X4ZUMMshsqlWyTHOcw7ns+GZmlqZZN05ZHcQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
|
||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
|
||||
@@ -415,8 +425,12 @@ github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/sasha-s/go-deadlock v0.3.9 h1:fiaT9rB7g5sr5ddNZvlwheclN9IP86eFW9WgqlEQV+w=
|
||||
github.com/sasha-s/go-deadlock v0.3.9/go.mod h1:KuZj51ZFmx42q/mPaYbRk0P1xcwe697zsJKE03vD4/Y=
|
||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
@@ -434,8 +448,6 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -498,6 +510,8 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||
@@ -520,6 +534,10 @@ go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpu
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
|
||||
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
@@ -541,8 +559,8 @@ golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -594,15 +612,13 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
|
||||
golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad h1:3iLyITS/sySRwbUKoC7ogfj2Yr1Cjs0pfaRKj5U5HEw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:KdNqO+rCIWgFumrNBSEDlDNrkrQnpkax7Tv1WxNY8V4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
oas "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
)
|
||||
|
||||
// CreateApiKey creates an API key and returns the full secret. This is the only
|
||||
// time the secret is exposed.
|
||||
func (s *Server) CreateApiKey(
|
||||
_ context.Context,
|
||||
req *oas.CreateApiKeyReq,
|
||||
) (*oas.CreateApiKeyOK, error) {
|
||||
var expiration time.Time
|
||||
if v, ok := req.Expiration.Get(); ok {
|
||||
expiration = v
|
||||
}
|
||||
|
||||
key, _, err := s.state.CreateAPIKey(&expiration)
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
return &oas.CreateApiKeyOK{ApiKey: oas.NewOptString(key)}, nil
|
||||
}
|
||||
|
||||
// ListApiKeys lists all API keys (masked), sorted by id.
|
||||
func (s *Server) ListApiKeys(_ context.Context) (*oas.ListApiKeysOK, error) {
|
||||
keys, err := s.state.ListAPIKeys()
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
slices.SortFunc(keys, func(a, b types.APIKey) int { return cmp.Compare(a.ID, b.ID) })
|
||||
|
||||
out := make([]oas.ApiKey, len(keys))
|
||||
for i := range keys {
|
||||
out[i] = oasAPIKey(&keys[i])
|
||||
}
|
||||
|
||||
return &oas.ListApiKeysOK{ApiKeys: out}, nil
|
||||
}
|
||||
|
||||
// ExpireApiKey expires an API key identified by id or prefix.
|
||||
func (s *Server) ExpireApiKey(_ context.Context, req *oas.ExpireApiKeyReq) error {
|
||||
key, apiErr := s.apiKeyByIDOrPrefix(req.ID.Or(0), req.Prefix.Or(""))
|
||||
if apiErr != nil {
|
||||
return apiErr
|
||||
}
|
||||
|
||||
err := s.state.ExpireAPIKey(key)
|
||||
if err != nil {
|
||||
return mapStateError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteApiKey deletes an API key identified by prefix (or id).
|
||||
func (s *Server) DeleteApiKey(_ context.Context, params oas.DeleteApiKeyParams) error {
|
||||
key, apiErr := s.apiKeyByIDOrPrefix(params.ID.Or(0), params.Prefix)
|
||||
if apiErr != nil {
|
||||
return apiErr
|
||||
}
|
||||
|
||||
err := s.state.DestroyAPIKey(*key)
|
||||
if err != nil {
|
||||
return mapStateError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// apiKeyByIDOrPrefix looks up an API key by exactly one of id or prefix.
|
||||
// Providing neither or both is a 400.
|
||||
func (s *Server) apiKeyByIDOrPrefix(
|
||||
id uint64,
|
||||
prefix string,
|
||||
) (*types.APIKey, *oas.ErrorStatusCode) {
|
||||
hasID := id != 0
|
||||
hasPrefix := prefix != ""
|
||||
|
||||
switch {
|
||||
case hasID && hasPrefix:
|
||||
return nil, badRequest("provide either id or prefix, not both")
|
||||
case hasID:
|
||||
key, err := s.state.GetAPIKeyByID(id)
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
case hasPrefix:
|
||||
key, err := s.state.GetAPIKey(prefix)
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
default:
|
||||
return nil, badRequest("must provide id or prefix")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
oas "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
)
|
||||
|
||||
var errAuthRejected = errors.New("auth request rejected")
|
||||
|
||||
type socketAuthContextKey struct{}
|
||||
|
||||
// WithSocketAuth marks ctx as originating from the trusted local unix socket,
|
||||
// where filesystem permissions are the trust boundary. Requests carrying this
|
||||
// marker bypass bearer-token validation. The server mounts this on the
|
||||
// socket-only listener.
|
||||
func WithSocketAuth(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, socketAuthContextKey{}, true)
|
||||
}
|
||||
|
||||
func isSocketAuth(ctx context.Context) bool {
|
||||
v, _ := ctx.Value(socketAuthContextKey{}).(bool)
|
||||
return v
|
||||
}
|
||||
|
||||
// HandleBearerAuth validates the API key bearer token against the state layer.
|
||||
// A missing or malformed Authorization header is reported by ogen before this
|
||||
// is reached. Any validation failure — a malformed/unknown key (which
|
||||
// [state.State.ValidateAPIKey] reports as an error) or an expired/invalid one —
|
||||
// is a 401, matching the previous middleware which rejected every such case
|
||||
// with Unauthorized.
|
||||
func (s *Server) HandleBearerAuth(
|
||||
ctx context.Context,
|
||||
_ oas.OperationName,
|
||||
t oas.BearerAuth,
|
||||
) (context.Context, error) {
|
||||
// Requests from the local unix socket are trusted via filesystem permissions.
|
||||
if isSocketAuth(ctx) {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
valid, err := s.state.ValidateAPIKey(t.Token)
|
||||
if err != nil || !valid {
|
||||
return ctx, apiError(http.StatusUnauthorized, "invalid API key")
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// AuthRegister registers a node via an auth id; it is an alias of RegisterNode.
|
||||
func (s *Server) AuthRegister(
|
||||
ctx context.Context,
|
||||
req *oas.AuthRegisterReq,
|
||||
) (*oas.AuthRegisterOK, error) {
|
||||
resp, err := s.RegisterNode(ctx, oas.RegisterNodeParams{
|
||||
Key: oas.NewOptString(req.AuthId.Or("")),
|
||||
User: oas.NewOptString(req.User.Or("")),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &oas.AuthRegisterOK{Node: resp.Node}, nil
|
||||
}
|
||||
|
||||
// AuthApprove approves a pending auth session.
|
||||
func (s *Server) AuthApprove(_ context.Context, req *oas.AuthApproveReq) error {
|
||||
authReq, apiErr := s.pendingAuth(req.AuthId.Or(""))
|
||||
if apiErr != nil {
|
||||
return apiErr
|
||||
}
|
||||
|
||||
authReq.FinishAuth(types.AuthVerdict{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthReject rejects a pending auth session.
|
||||
func (s *Server) AuthReject(_ context.Context, req *oas.AuthRejectReq) error {
|
||||
authReq, apiErr := s.pendingAuth(req.AuthId.Or(""))
|
||||
if apiErr != nil {
|
||||
return apiErr
|
||||
}
|
||||
|
||||
authReq.FinishAuth(types.AuthVerdict{Err: errAuthRejected})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// pendingAuth resolves an auth id to its cached, in-progress auth request.
|
||||
// An unparseable id is a 400; an unknown one is a 404.
|
||||
func (s *Server) pendingAuth(authID string) (*types.AuthRequest, *oas.ErrorStatusCode) {
|
||||
id, err := types.AuthIDFromString(authID)
|
||||
if err != nil {
|
||||
return nil, badRequest("invalid auth_id: " + err.Error())
|
||||
}
|
||||
|
||||
authReq, ok := s.state.GetAuthCacheEntry(id)
|
||||
if !ok {
|
||||
return nil, notFound("no pending auth session for auth_id " + id.String())
|
||||
}
|
||||
|
||||
return authReq, nil
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
oas "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
// This file converts the state-layer types into the ogen API types. It reads
|
||||
// the copy-on-write view types (NodeView, UserView, PreAuthKeyView) directly so
|
||||
// no node/user is deep-copied on the read path, and reproduces the same fields
|
||||
// the previous API emitted — username fallback, masked key prefixes, online
|
||||
// computation, the register-method enum.
|
||||
//
|
||||
// These converters omit zero-value and absent fields — empty strings, false booleans, zero
|
||||
// numbers, empty arrays, nil timestamps/objects, and the unspecified register
|
||||
// method. See docs/v1-ogen/CHANGES.md.
|
||||
|
||||
func optString(s string) oas.OptString {
|
||||
if s == "" {
|
||||
return oas.OptString{}
|
||||
}
|
||||
|
||||
return oas.NewOptString(s)
|
||||
}
|
||||
|
||||
func optUint64(v uint64) oas.OptUint64 {
|
||||
if v == 0 {
|
||||
return oas.OptUint64{}
|
||||
}
|
||||
|
||||
return oas.NewOptUint64(v)
|
||||
}
|
||||
|
||||
func optBool(b bool) oas.OptBool {
|
||||
if !b {
|
||||
return oas.OptBool{}
|
||||
}
|
||||
|
||||
return oas.NewOptBool(b)
|
||||
}
|
||||
|
||||
// optTimeVal sets a timestamp from a value; the previous proto builders always
|
||||
// emitted these fields (timestamppb.New is never nil), so they are always set.
|
||||
func optTimeVal(t time.Time) oas.OptDateTime {
|
||||
return oas.NewOptDateTime(t)
|
||||
}
|
||||
|
||||
// optTimePtr sets a timestamp only when present, matching the proto builders'
|
||||
// nil checks.
|
||||
func optTimePtr(t *time.Time) oas.OptDateTime {
|
||||
if t == nil {
|
||||
return oas.OptDateTime{}
|
||||
}
|
||||
|
||||
return oas.NewOptDateTime(*t)
|
||||
}
|
||||
|
||||
// optTimeVP sets a timestamp from a view's optional pointer.
|
||||
func optTimeVP(p views.ValuePointer[time.Time]) oas.OptDateTime {
|
||||
if !p.Valid() {
|
||||
return oas.OptDateTime{}
|
||||
}
|
||||
|
||||
return oas.NewOptDateTime(p.Get())
|
||||
}
|
||||
|
||||
// strs normalises an empty slice to nil so it is omitted from the response
|
||||
// rather than emitted as an empty array.
|
||||
func strs(s []string) []string {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func oasRegisterMethod(rm string) oas.OptRegisterMethod {
|
||||
var v oas.RegisterMethod
|
||||
|
||||
switch rm {
|
||||
case "authkey":
|
||||
v = "REGISTER_METHOD_AUTH_KEY"
|
||||
case "oidc":
|
||||
v = "REGISTER_METHOD_OIDC"
|
||||
case "cli":
|
||||
v = "REGISTER_METHOD_CLI"
|
||||
default:
|
||||
return oas.OptRegisterMethod{}
|
||||
}
|
||||
|
||||
return oas.NewOptRegisterMethod(v)
|
||||
}
|
||||
|
||||
func oasUser(u types.UserView) oas.User {
|
||||
// Use Name if set, otherwise the display-friendly Username() fallback.
|
||||
name := u.Name()
|
||||
if name == "" {
|
||||
name = u.Username()
|
||||
}
|
||||
|
||||
return oas.User{
|
||||
ID: optUint64(uint64(u.Model().ID)),
|
||||
Name: optString(name),
|
||||
CreatedAt: optTimeVal(u.Model().CreatedAt),
|
||||
DisplayName: optString(u.DisplayName()),
|
||||
Email: optString(u.Email()),
|
||||
ProviderId: optString(u.ProviderIdentifier().String),
|
||||
Provider: optString(u.Provider()),
|
||||
ProfilePicUrl: optString(u.ProfilePicURL()),
|
||||
}
|
||||
}
|
||||
|
||||
func optUser(u types.UserView) oas.OptUser {
|
||||
if !u.Valid() {
|
||||
return oas.OptUser{}
|
||||
}
|
||||
|
||||
return oas.NewOptUser(oasUser(u))
|
||||
}
|
||||
|
||||
func apiKeyMaskedPrefix(prefix string) string {
|
||||
if len(prefix) == types.NewAPIKeyPrefixLength {
|
||||
return "hskey-api-" + prefix + "-***"
|
||||
}
|
||||
|
||||
return prefix + "***"
|
||||
}
|
||||
|
||||
func oasAPIKey(k *types.APIKey) oas.ApiKey {
|
||||
return oas.ApiKey{
|
||||
ID: optUint64(k.ID),
|
||||
Prefix: optString(apiKeyMaskedPrefix(k.Prefix)),
|
||||
Expiration: optTimePtr(k.Expiration),
|
||||
CreatedAt: optTimePtr(k.CreatedAt),
|
||||
LastSeen: optTimePtr(k.LastSeen),
|
||||
}
|
||||
}
|
||||
|
||||
func preAuthKeyMaskedPrefix(prefix string) string {
|
||||
if prefix != "" {
|
||||
return "hskey-auth-" + prefix + "-***"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func oasPreAuthKey(k types.PreAuthKeyView) oas.PreAuthKey {
|
||||
out := oas.PreAuthKey{
|
||||
User: optUser(k.User()),
|
||||
ID: optUint64(k.ID()),
|
||||
Reusable: optBool(k.Reusable()),
|
||||
Ephemeral: optBool(k.Ephemeral()),
|
||||
Used: optBool(k.Used()),
|
||||
Expiration: optTimeVP(k.Expiration()),
|
||||
CreatedAt: optTimeVP(k.CreatedAt()),
|
||||
AclTags: strs(k.Tags().AsSlice()),
|
||||
}
|
||||
|
||||
// New keys show the masked prefix; legacy keys (with a plaintext key) show
|
||||
// the full key for backwards compatibility.
|
||||
if masked := preAuthKeyMaskedPrefix(k.Prefix()); masked != "" {
|
||||
out.Key = optString(masked)
|
||||
} else if k.Key() != "" {
|
||||
out.Key = optString(k.Key())
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// oasPreAuthKeyNew converts a freshly created pre-auth key, which exposes the
|
||||
// full secret key exactly once and has no view type.
|
||||
func oasPreAuthKeyNew(k *types.PreAuthKeyNew) oas.PreAuthKey {
|
||||
out := oas.PreAuthKey{
|
||||
ID: optUint64(k.ID),
|
||||
Key: optString(k.Key),
|
||||
Reusable: optBool(k.Reusable),
|
||||
Ephemeral: optBool(k.Ephemeral),
|
||||
Expiration: optTimePtr(k.Expiration),
|
||||
CreatedAt: optTimePtr(k.CreatedAt),
|
||||
AclTags: strs(k.Tags),
|
||||
}
|
||||
|
||||
if k.User != nil {
|
||||
out.User = oas.NewOptUser(oasUser(k.User.View()))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// oasNode converts a node view. user is the user to present (the node's own
|
||||
// user for most callers, or TaggedDevices for tagged nodes); subnetRoutes
|
||||
// carries the routes actively served from the node (primary + exit), which only
|
||||
// some callers populate.
|
||||
func oasNode(nv types.NodeView, user types.UserView, subnetRoutes []string) oas.Node {
|
||||
out := oas.Node{
|
||||
ID: optUint64(uint64(nv.ID())),
|
||||
MachineKey: optString(nv.MachineKey().String()),
|
||||
NodeKey: optString(nv.NodeKey().String()),
|
||||
DiscoKey: optString(nv.DiscoKey().String()),
|
||||
IpAddresses: strs(nv.IPsAsString()),
|
||||
Name: optString(nv.Hostname()),
|
||||
User: optUser(user),
|
||||
CreatedAt: optTimeVal(nv.CreatedAt()),
|
||||
RegisterMethod: oasRegisterMethod(nv.RegisterMethod()),
|
||||
GivenName: optString(nv.GivenName()),
|
||||
Online: optBool(nv.IsOnline().GetOr(false)),
|
||||
ApprovedRoutes: strs(util.PrefixesToString(nv.ApprovedRoutes().AsSlice())),
|
||||
AvailableRoutes: strs(util.PrefixesToString(nv.AnnouncedRoutes())),
|
||||
SubnetRoutes: strs(subnetRoutes),
|
||||
Tags: strs(nv.Tags().AsSlice()),
|
||||
LastSeen: optTimeVP(nv.LastSeen()),
|
||||
Expiry: optTimeVP(nv.Expiry()),
|
||||
}
|
||||
|
||||
if nv.AuthKey().Valid() {
|
||||
out.PreAuthKey = oas.NewOptPreAuthKey(oasPreAuthKey(nv.AuthKey()))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
oas "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/db"
|
||||
"github.com/juanfont/headscale/hscontrol/state"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/ogen-go/ogen/ogenerrors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const problemContentType = "application/problem+json"
|
||||
|
||||
// apiError builds an RFC 7807 problem response with the given HTTP status and
|
||||
// detail. Handlers return it so expected errors render as problem documents
|
||||
// with the correct status code (ogen encodes [oas.ErrorStatusCode] directly,
|
||||
// without going through [errorHandler]).
|
||||
func apiError(status int, detail string) *oas.ErrorStatusCode {
|
||||
return &oas.ErrorStatusCode{
|
||||
StatusCode: status,
|
||||
Response: oas.Problem{
|
||||
Title: oas.NewOptString(http.StatusText(status)),
|
||||
//nolint:gosec // G115: status is an HTTP status code, always within int32.
|
||||
Status: oas.NewOptInt32(int32(status)),
|
||||
Detail: oas.NewOptString(detail),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func notFound(
|
||||
detail string,
|
||||
) *oas.ErrorStatusCode {
|
||||
return apiError(http.StatusNotFound, detail)
|
||||
}
|
||||
|
||||
func badRequest(
|
||||
detail string,
|
||||
) *oas.ErrorStatusCode {
|
||||
return apiError(http.StatusBadRequest, detail)
|
||||
}
|
||||
|
||||
func internalError(detail string) *oas.ErrorStatusCode {
|
||||
return apiError(http.StatusInternalServerError, detail)
|
||||
}
|
||||
|
||||
// mapStateError classifies an error returned by the state layer into an HTTP
|
||||
// problem response. Not-found sentinels become 404; everything else is a 500.
|
||||
// Handlers that need a different status (e.g. validation 400) build the problem
|
||||
// explicitly with [badRequest] rather than routing through here.
|
||||
func mapStateError(err error) *oas.ErrorStatusCode {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound),
|
||||
errors.Is(err, state.ErrNodeNotFound),
|
||||
errors.Is(err, state.ErrNodeNotInNodeStore),
|
||||
errors.Is(err, db.ErrUserNotFound):
|
||||
return notFound(err.Error())
|
||||
case errors.Is(err, types.ErrPolicyUpdateIsDisabled):
|
||||
return badRequest(err.Error())
|
||||
default:
|
||||
return internalError(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// NewError converts an error that ogen raises outside a handler return value —
|
||||
// failed authentication, request decoding — into a typed problem response. ogen
|
||||
// calls it from the generated security/decoding paths; the resulting
|
||||
// [oas.ErrorStatusCode] is then encoded as application/problem+json.
|
||||
func (s *Server) NewError(_ context.Context, err error) *oas.ErrorStatusCode {
|
||||
return classify(err)
|
||||
}
|
||||
|
||||
// errorHandler renders problems for the remaining ogen error path: a plain
|
||||
// error returned from a handler. Expected errors are returned as
|
||||
// [oas.ErrorStatusCode] (encoded directly by ogen), so this is the safety net
|
||||
// for anything else.
|
||||
func errorHandler(
|
||||
_ context.Context,
|
||||
w http.ResponseWriter,
|
||||
_ *http.Request,
|
||||
err error,
|
||||
) {
|
||||
writeProblem(w, classify(err))
|
||||
}
|
||||
|
||||
// classify maps an arbitrary error to a problem response. An already-typed
|
||||
// [oas.ErrorStatusCode] passes through; ogen's framework errors carry their own
|
||||
// HTTP status via Code() (security -> 401, decode -> 400); everything else is
|
||||
// classified by [mapStateError].
|
||||
func classify(err error) *oas.ErrorStatusCode {
|
||||
var esc *oas.ErrorStatusCode
|
||||
if errors.As(err, &esc) {
|
||||
return esc
|
||||
}
|
||||
|
||||
// A failed security requirement (missing/malformed bearer) must not echo
|
||||
// ogen's internal "operation X: security ...: not satisfied" message, which
|
||||
// leaks the operation name. Return a minimal 401.
|
||||
var secErr *ogenerrors.SecurityError
|
||||
if errors.As(err, &secErr) {
|
||||
return apiError(http.StatusUnauthorized, "valid API key required")
|
||||
}
|
||||
|
||||
var coder interface{ Code() int }
|
||||
if errors.As(err, &coder) {
|
||||
return apiError(coder.Code(), err.Error())
|
||||
}
|
||||
|
||||
return mapStateError(err)
|
||||
}
|
||||
|
||||
func writeProblem(w http.ResponseWriter, esc *oas.ErrorStatusCode) {
|
||||
w.Header().Set("Content-Type", problemContentType)
|
||||
w.WriteHeader(esc.StatusCode)
|
||||
|
||||
p := esc.Response
|
||||
|
||||
status := esc.StatusCode
|
||||
if v, ok := p.Status.Get(); ok {
|
||||
status = int(v)
|
||||
}
|
||||
|
||||
body := problemJSON{
|
||||
Title: p.Title.Or(""),
|
||||
Status: status,
|
||||
Detail: p.Detail.Or(""),
|
||||
}
|
||||
if v, ok := p.Type.Get(); ok {
|
||||
body.Type = v.String()
|
||||
}
|
||||
|
||||
if v, ok := p.Instance.Get(); ok {
|
||||
body.Instance = v.String()
|
||||
}
|
||||
|
||||
err := json.NewEncoder(w).Encode(body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("writing problem response failed")
|
||||
}
|
||||
}
|
||||
|
||||
// problemJSON mirrors [oas.Problem] for hand-written encoding in
|
||||
// [errorHandler]; ogen's own encoder is package-private.
|
||||
type problemJSON struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Status int `json:"status,omitempty"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
Instance string `json:"instance,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ogen-go/ogen/ogenerrors"
|
||||
)
|
||||
|
||||
var errSecurityNotSatisfied = errors.New(
|
||||
`operation ListUsers: security "": security requirement is not satisfied`,
|
||||
)
|
||||
|
||||
// TestClassifySecurityErrorIsMinimal ensures a failed security requirement
|
||||
// becomes a clean 401 that does not leak ogen's internal operation/security
|
||||
// message.
|
||||
func TestClassifySecurityErrorIsMinimal(t *testing.T) {
|
||||
secErr := &ogenerrors.SecurityError{Err: errSecurityNotSatisfied}
|
||||
|
||||
esc := classify(secErr)
|
||||
|
||||
if esc.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("status = %d, want 401", esc.StatusCode)
|
||||
}
|
||||
|
||||
detail := esc.Response.Detail.Or("")
|
||||
if strings.Contains(detail, "operation") || strings.Contains(detail, "ListUsers") {
|
||||
t.Errorf("401 detail leaks internals: %q", detail)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
oas "github.com/juanfont/headscale/gen/api/v1"
|
||||
)
|
||||
|
||||
// Health reports server health, including database connectivity. A failed
|
||||
// database ping is a 500; the previous implementation likewise returned the ping
|
||||
// error (the body's databaseConnectivity flag was never observable on failure).
|
||||
func (s *Server) Health(ctx context.Context) (*oas.HealthOK, error) {
|
||||
err := s.state.PingDB(ctx)
|
||||
if err != nil {
|
||||
return nil, internalError("pinging database: " + err.Error())
|
||||
}
|
||||
|
||||
return &oas.HealthOK{DatabaseConnectivity: oas.NewOptBool(true)}, nil
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
oas "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
// RegisterNode registers a node to a user using a registration id, then
|
||||
// auto-approves its routes.
|
||||
func (s *Server) RegisterNode(
|
||||
_ context.Context,
|
||||
params oas.RegisterNodeParams,
|
||||
) (*oas.RegisterNodeOK, error) {
|
||||
registrationID, err := types.AuthIDFromString(params.Key.Or(""))
|
||||
if err != nil {
|
||||
return nil, badRequest(err.Error())
|
||||
}
|
||||
|
||||
user, err := s.state.GetUserByName(params.User.Or(""))
|
||||
if err != nil {
|
||||
return nil, mapStateError(fmt.Errorf("looking up user: %w", err))
|
||||
}
|
||||
|
||||
node, nodeChange, err := s.state.HandleNodeFromAuthPath(
|
||||
registrationID,
|
||||
types.UserID(user.ID),
|
||||
nil,
|
||||
util.RegisterMethodCLI,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
routeChange, err := s.state.AutoApproveRoutes(node)
|
||||
if err != nil {
|
||||
return nil, internalError("auto approving routes: " + err.Error())
|
||||
}
|
||||
|
||||
s.change(nodeChange, routeChange)
|
||||
|
||||
return &oas.RegisterNodeOK{Node: oas.NewOptNode(oasNode(node, node.User(), nil))}, nil
|
||||
}
|
||||
|
||||
// GetNode returns a node by id.
|
||||
func (s *Server) GetNode(_ context.Context, params oas.GetNodeParams) (*oas.GetNodeOK, error) {
|
||||
node, ok := s.state.GetNodeByID(types.NodeID(params.NodeID))
|
||||
if !ok {
|
||||
return nil, notFound("node not found")
|
||||
}
|
||||
|
||||
return &oas.GetNodeOK{Node: oas.NewOptNode(oasNode(node, node.User(), nil))}, nil
|
||||
}
|
||||
|
||||
// SetTags sets the ACL tags of a node, converting it to a tagged node.
|
||||
func (s *Server) SetTags(
|
||||
_ context.Context,
|
||||
req *oas.SetTagsReq,
|
||||
params oas.SetTagsParams,
|
||||
) (*oas.SetTagsOK, error) {
|
||||
if len(req.Tags) == 0 {
|
||||
return nil, badRequest(
|
||||
"cannot remove all tags from a node - tagged nodes must have at least one tag",
|
||||
)
|
||||
}
|
||||
|
||||
for _, tag := range req.Tags {
|
||||
err := validateTag(tag)
|
||||
if err != nil {
|
||||
return nil, badRequest(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
_, found := s.state.GetNodeByID(types.NodeID(params.NodeID))
|
||||
if !found {
|
||||
return nil, notFound("node not found")
|
||||
}
|
||||
|
||||
node, nodeChange, err := s.state.SetNodeTags(types.NodeID(params.NodeID), req.Tags)
|
||||
if err != nil {
|
||||
return nil, badRequest(err.Error())
|
||||
}
|
||||
|
||||
s.change(nodeChange)
|
||||
|
||||
return &oas.SetTagsOK{Node: oas.NewOptNode(oasNode(node, node.User(), nil))}, nil
|
||||
}
|
||||
|
||||
// SetApprovedRoutes sets the approved subnet routes of a node, expanding exit
|
||||
// routes to cover both address families.
|
||||
func (s *Server) SetApprovedRoutes(
|
||||
_ context.Context,
|
||||
req *oas.SetApprovedRoutesReq,
|
||||
params oas.SetApprovedRoutesParams,
|
||||
) (*oas.SetApprovedRoutesOK, error) {
|
||||
var newApproved []netip.Prefix
|
||||
|
||||
for _, route := range req.Routes {
|
||||
prefix, err := netip.ParsePrefix(route)
|
||||
if err != nil {
|
||||
return nil, badRequest("parsing route: " + err.Error())
|
||||
}
|
||||
|
||||
// An exit route is annotated by both v4 and v6 default routes.
|
||||
if prefix == tsaddr.AllIPv4() || prefix == tsaddr.AllIPv6() {
|
||||
newApproved = append(newApproved, tsaddr.AllIPv4(), tsaddr.AllIPv6())
|
||||
} else {
|
||||
newApproved = append(newApproved, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortFunc(newApproved, netip.Prefix.Compare)
|
||||
newApproved = slices.Compact(newApproved)
|
||||
|
||||
node, nodeChange, err := s.state.SetApprovedRoutes(types.NodeID(params.NodeID), newApproved)
|
||||
if err != nil {
|
||||
return nil, badRequest(err.Error())
|
||||
}
|
||||
|
||||
s.change(nodeChange)
|
||||
|
||||
// SubnetRoutes carries only the routes actively served from the node.
|
||||
subnetRoutes := util.PrefixesToString(s.state.GetNodePrimaryRoutes(node.ID()))
|
||||
|
||||
return &oas.SetApprovedRoutesOK{
|
||||
Node: oas.NewOptNode(oasNode(node, node.User(), subnetRoutes)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteNode deletes a node.
|
||||
func (s *Server) DeleteNode(_ context.Context, params oas.DeleteNodeParams) error {
|
||||
node, ok := s.state.GetNodeByID(types.NodeID(params.NodeID))
|
||||
if !ok {
|
||||
return notFound("node not found")
|
||||
}
|
||||
|
||||
nodeChange, err := s.state.DeleteNode(node)
|
||||
if err != nil {
|
||||
return mapStateError(err)
|
||||
}
|
||||
|
||||
s.change(nodeChange)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExpireNode expires a node, or disables its expiry.
|
||||
func (s *Server) ExpireNode(
|
||||
_ context.Context,
|
||||
params oas.ExpireNodeParams,
|
||||
) (*oas.ExpireNodeOK, error) {
|
||||
_, hasExpiry := params.Expiry.Get()
|
||||
if params.DisableExpiry.Or(false) && hasExpiry {
|
||||
return nil, badRequest("cannot set both disable_expiry and expiry")
|
||||
}
|
||||
|
||||
var expiry *time.Time
|
||||
|
||||
if !params.DisableExpiry.Or(false) {
|
||||
t := time.Now()
|
||||
if v, ok := params.Expiry.Get(); ok {
|
||||
t = v
|
||||
}
|
||||
|
||||
expiry = &t
|
||||
}
|
||||
|
||||
node, nodeChange, err := s.state.SetNodeExpiry(types.NodeID(params.NodeID), expiry)
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
s.change(nodeChange)
|
||||
|
||||
return &oas.ExpireNodeOK{Node: oas.NewOptNode(oasNode(node, node.User(), nil))}, nil
|
||||
}
|
||||
|
||||
// RenameNode renames a node.
|
||||
func (s *Server) RenameNode(
|
||||
_ context.Context,
|
||||
params oas.RenameNodeParams,
|
||||
) (*oas.RenameNodeOK, error) {
|
||||
node, nodeChange, err := s.state.RenameNode(types.NodeID(params.NodeID), params.NewName)
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
s.change(nodeChange)
|
||||
|
||||
return &oas.RenameNodeOK{Node: oas.NewOptNode(oasNode(node, node.User(), nil))}, nil
|
||||
}
|
||||
|
||||
// ListNodes lists nodes, optionally filtered by user, sorted by id.
|
||||
func (s *Server) ListNodes(
|
||||
_ context.Context,
|
||||
params oas.ListNodesParams,
|
||||
) (*oas.ListNodesOK, error) {
|
||||
var nodes views.Slice[types.NodeView]
|
||||
|
||||
if params.User.Or("") != "" {
|
||||
user, err := s.state.GetUserByName(params.User.Or(""))
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
nodes = s.state.ListNodesByUser(types.UserID(user.ID))
|
||||
} else {
|
||||
nodes = s.state.ListNodes()
|
||||
}
|
||||
|
||||
return &oas.ListNodesOK{Nodes: s.nodesToOAS(nodes)}, nil
|
||||
}
|
||||
|
||||
// nodesToOAS converts a slice of node views to API nodes, presenting tagged
|
||||
// nodes as the TaggedDevices user and populating SubnetRoutes with the routes
|
||||
// actively served from each node.
|
||||
func (s *Server) nodesToOAS(nodes views.Slice[types.NodeView]) []oas.Node {
|
||||
out := make([]oas.Node, nodes.Len())
|
||||
|
||||
for index, node := range nodes.All() {
|
||||
// Tagged nodes are presented as the TaggedDevices user.
|
||||
user := node.User()
|
||||
if node.IsTagged() {
|
||||
user = types.TaggedDevices.View()
|
||||
}
|
||||
|
||||
subnetRoutes := util.PrefixesToString(
|
||||
append(s.state.GetNodePrimaryRoutes(node.ID()), node.ExitRoutes()...),
|
||||
)
|
||||
|
||||
out[index] = oasNode(node, user, subnetRoutes)
|
||||
}
|
||||
|
||||
slices.SortFunc(out, func(a, b oas.Node) int { return cmp.Compare(a.ID.Or(0), b.ID.Or(0)) })
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// BackfillNodeIPs backfills missing IP addresses for all nodes. It must be
|
||||
// explicitly confirmed.
|
||||
func (s *Server) BackfillNodeIPs(
|
||||
_ context.Context,
|
||||
params oas.BackfillNodeIPsParams,
|
||||
) (*oas.BackfillNodeIPsOK, error) {
|
||||
if !params.Confirmed.Or(false) {
|
||||
return nil, badRequest("not confirmed, aborting")
|
||||
}
|
||||
|
||||
changes, err := s.state.BackfillNodeIPs()
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
return &oas.BackfillNodeIPsOK{Changes: changes}, nil
|
||||
}
|
||||
|
||||
// DebugCreateNode caches a synthetic node registration for testing and echoes
|
||||
// back a node describing it. The real node is created later via AuthApprove.
|
||||
func (s *Server) DebugCreateNode(
|
||||
_ context.Context,
|
||||
req *oas.DebugCreateNodeReq,
|
||||
) (*oas.DebugCreateNodeOK, error) {
|
||||
user, err := s.state.GetUserByName(req.User.Or(""))
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
routes, err := util.StringToIPPrefix(req.Routes)
|
||||
if err != nil {
|
||||
return nil, badRequest(err.Error())
|
||||
}
|
||||
|
||||
registrationID, err := types.AuthIDFromString(req.Key.Or(""))
|
||||
if err != nil {
|
||||
return nil, badRequest(err.Error())
|
||||
}
|
||||
|
||||
regData := &types.RegistrationData{
|
||||
NodeKey: key.NewNode().Public(),
|
||||
MachineKey: key.NewMachine().Public(),
|
||||
Hostname: req.Name.Or(""),
|
||||
Expiry: &time.Time{},
|
||||
}
|
||||
|
||||
s.state.SetAuthCacheEntry(registrationID, types.NewRegisterAuthRequest(regData))
|
||||
|
||||
echoNode := types.Node{
|
||||
NodeKey: regData.NodeKey,
|
||||
MachineKey: regData.MachineKey,
|
||||
Hostname: regData.Hostname,
|
||||
User: user,
|
||||
Expiry: &time.Time{},
|
||||
LastSeen: &time.Time{},
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: req.Name.Or(""),
|
||||
OS: "TestOS",
|
||||
RoutableIPs: routes,
|
||||
},
|
||||
}
|
||||
|
||||
return &oas.DebugCreateNodeOK{Node: oas.NewOptNode(oasNode(echoNode.View(), echoNode.View().User(), nil))}, nil
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
oas "github.com/juanfont/headscale/gen/api/v1"
|
||||
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
)
|
||||
|
||||
// GetPolicy returns the current ACL policy, from the database or the policy
|
||||
// file depending on the configured policy mode.
|
||||
func (s *Server) GetPolicy(_ context.Context) (*oas.GetPolicyOK, error) {
|
||||
switch s.cfg.Policy.Mode {
|
||||
case types.PolicyModeDB:
|
||||
p, err := s.state.GetPolicy()
|
||||
if err != nil {
|
||||
return nil, internalError("loading ACL from database: " + err.Error())
|
||||
}
|
||||
|
||||
return &oas.GetPolicyOK{
|
||||
Policy: oas.NewOptString(p.Data),
|
||||
UpdatedAt: oas.NewOptDateTime(p.UpdatedAt),
|
||||
}, nil
|
||||
case types.PolicyModeFile:
|
||||
absPath := util.AbsolutePathFromConfigPath(s.cfg.Policy.Path)
|
||||
|
||||
b, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return nil, internalError("reading policy from path " + absPath + ": " + err.Error())
|
||||
}
|
||||
|
||||
return &oas.GetPolicyOK{Policy: oas.NewOptString(string(b))}, nil
|
||||
}
|
||||
|
||||
return nil, internalError(
|
||||
"no supported policy mode found in configuration, policy.mode: " +
|
||||
string(s.cfg.Policy.Mode),
|
||||
)
|
||||
}
|
||||
|
||||
// SetPolicy stores a new ACL policy (database policy mode only), validating it
|
||||
// against the live nodes and distributing the resulting changes.
|
||||
func (s *Server) SetPolicy(_ context.Context, req *oas.SetPolicyReq) (*oas.SetPolicyOK, error) {
|
||||
if s.cfg.Policy.Mode != types.PolicyModeDB {
|
||||
return nil, badRequest(types.ErrPolicyUpdateIsDisabled.Error())
|
||||
}
|
||||
|
||||
p := req.Policy.Or("")
|
||||
|
||||
// Validate against live nodes, where they exist, before storing.
|
||||
nodes := s.state.ListNodes()
|
||||
|
||||
_, err := s.state.SetPolicy([]byte(p))
|
||||
if err != nil {
|
||||
return nil, badRequest("setting policy: " + err.Error())
|
||||
}
|
||||
|
||||
if nodes.Len() > 0 {
|
||||
_, err = s.state.SSHPolicy(nodes.At(0))
|
||||
if err != nil {
|
||||
return nil, badRequest("verifying SSH rules: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := s.state.SetPolicyInDB(p)
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
// Always reload so routes are re-evaluated even when the content is unchanged.
|
||||
cs, err := s.state.ReloadPolicy()
|
||||
if err != nil {
|
||||
return nil, internalError("reloading policy: " + err.Error())
|
||||
}
|
||||
|
||||
if len(cs) > 0 {
|
||||
s.change(cs...)
|
||||
}
|
||||
|
||||
return &oas.SetPolicyOK{
|
||||
Policy: oas.NewOptString(updated.Data),
|
||||
UpdatedAt: oas.NewOptDateTime(updated.UpdatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CheckPolicy validates a policy against the live users and nodes without
|
||||
// storing it. Works regardless of policy mode.
|
||||
func (s *Server) CheckPolicy(_ context.Context, req *oas.CheckPolicyReq) error {
|
||||
polB := []byte(req.Policy.Or(""))
|
||||
|
||||
users, err := s.state.ListAllUsers()
|
||||
if err != nil {
|
||||
return internalError("loading users: " + err.Error())
|
||||
}
|
||||
|
||||
nodes := s.state.ListNodes()
|
||||
|
||||
pm, err := policyv2.NewPolicyManager(polB, users, nodes)
|
||||
if err != nil {
|
||||
return badRequest(err.Error())
|
||||
}
|
||||
|
||||
_, err = pm.SetPolicy(polB)
|
||||
if err != nil {
|
||||
return badRequest(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
oas "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
)
|
||||
|
||||
// CreatePreAuthKey creates a pre-auth key for a user.
|
||||
func (s *Server) CreatePreAuthKey(
|
||||
_ context.Context,
|
||||
req *oas.CreatePreAuthKeyReq,
|
||||
) (*oas.CreatePreAuthKeyOK, error) {
|
||||
var expiration time.Time
|
||||
if v, ok := req.Expiration.Get(); ok {
|
||||
expiration = v
|
||||
}
|
||||
|
||||
for _, tag := range req.AclTags {
|
||||
err := validateTag(tag)
|
||||
if err != nil {
|
||||
return nil, badRequest(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
var userID *types.UserID
|
||||
|
||||
if req.User.Or(0) != 0 {
|
||||
user, err := s.state.GetUserByID(types.UserID(req.User.Or(0)))
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
userID = user.TypedID()
|
||||
}
|
||||
|
||||
preAuthKey, err := s.state.CreatePreAuthKey(
|
||||
userID,
|
||||
req.Reusable.Or(false),
|
||||
req.Ephemeral.Or(false),
|
||||
&expiration,
|
||||
req.AclTags,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
return &oas.CreatePreAuthKeyOK{
|
||||
PreAuthKey: oas.NewOptPreAuthKey(oasPreAuthKeyNew(preAuthKey)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListPreAuthKeys lists all pre-auth keys, sorted by id.
|
||||
func (s *Server) ListPreAuthKeys(_ context.Context) (*oas.ListPreAuthKeysOK, error) {
|
||||
keys, err := s.state.ListPreAuthKeys()
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
slices.SortFunc(keys, func(a, b types.PreAuthKey) int { return cmp.Compare(a.ID, b.ID) })
|
||||
|
||||
out := make([]oas.PreAuthKey, len(keys))
|
||||
for i := range keys {
|
||||
out[i] = oasPreAuthKey(keys[i].View())
|
||||
}
|
||||
|
||||
return &oas.ListPreAuthKeysOK{PreAuthKeys: out}, nil
|
||||
}
|
||||
|
||||
// ExpirePreAuthKey expires a pre-auth key.
|
||||
func (s *Server) ExpirePreAuthKey(_ context.Context, req *oas.ExpirePreAuthKeyReq) error {
|
||||
err := s.state.ExpirePreAuthKey(req.ID.Or(0))
|
||||
if err != nil {
|
||||
return mapStateError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePreAuthKey deletes a pre-auth key.
|
||||
func (s *Server) DeletePreAuthKey(_ context.Context, params oas.DeletePreAuthKeyParams) error {
|
||||
err := s.state.DeletePreAuthKey(params.ID.Or(0))
|
||||
if err != nil {
|
||||
return mapStateError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
errTagPrefix = errors.New("tag must start with the string 'tag:'")
|
||||
errTagLowercase = errors.New("tag should be lowercase")
|
||||
errTagSpaces = errors.New("tags must not contain spaces")
|
||||
)
|
||||
|
||||
// validateTag enforces the ACL tag format ("tag:" prefix, lowercase, no spaces).
|
||||
func validateTag(tag string) error {
|
||||
if !strings.HasPrefix(tag, "tag:") {
|
||||
return errTagPrefix
|
||||
}
|
||||
|
||||
if strings.ToLower(tag) != tag {
|
||||
return errTagLowercase
|
||||
}
|
||||
|
||||
if len(strings.Fields(tag)) > 1 {
|
||||
return errTagSpaces
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Package apiv1 implements the Headscale v1 HTTP API: thin handlers that
|
||||
// adapt the ogen-generated server interface ([oas.Handler]) onto the shared
|
||||
// state layer ([state.State]). Business logic lives in the state layer; these
|
||||
// handlers only translate between HTTP request/response types and state calls.
|
||||
//
|
||||
// The package deliberately does not import the parent hscontrol package: it
|
||||
// depends only on state, types, and the generated API package, so that
|
||||
// hscontrol can mount it without an import cycle.
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
oas "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/state"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/types/change"
|
||||
)
|
||||
|
||||
// Server implements the generated [oas.Handler] and [oas.SecurityHandler].
|
||||
//
|
||||
// Any operation not implemented here is inherited from
|
||||
// [oas.UnimplementedHandler] and returns 501; every operation is implemented.
|
||||
type Server struct {
|
||||
oas.UnimplementedHandler
|
||||
|
||||
state *state.State
|
||||
cfg *types.Config
|
||||
change changeFunc
|
||||
}
|
||||
|
||||
// changeFunc distributes state changes to connected nodes. In production this
|
||||
// is [github.com/juanfont/headscale/hscontrol.Headscale.Change]; tests may pass
|
||||
// a no-op or a recorder.
|
||||
type changeFunc func(...change.Change)
|
||||
|
||||
// NewHandler builds the v1 API as an [http.Handler] ready to mount at
|
||||
// /api/v1. changeFn distributes [change.Change]s produced by mutating
|
||||
// operations; it must not be nil (pass a no-op if changes are irrelevant).
|
||||
func NewHandler(
|
||||
st *state.State,
|
||||
cfg *types.Config,
|
||||
changeFn func(...change.Change),
|
||||
) (http.Handler, error) {
|
||||
s := &Server{
|
||||
state: st,
|
||||
cfg: cfg,
|
||||
change: changeFn,
|
||||
}
|
||||
|
||||
return oas.NewServer(s, s, oas.WithErrorHandler(errorHandler))
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
oas "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CreateUser creates a user and distributes the resulting policy change.
|
||||
func (s *Server) CreateUser(
|
||||
_ context.Context,
|
||||
req *oas.CreateUserReq,
|
||||
) (*oas.CreateUserOK, error) {
|
||||
newUser := types.User{
|
||||
Name: req.Name.Or(""),
|
||||
DisplayName: req.DisplayName.Or(""),
|
||||
Email: req.Email.Or(""),
|
||||
ProfilePicURL: req.PictureUrl.Or(""),
|
||||
}
|
||||
|
||||
user, policyChanged, err := s.state.CreateUser(newUser)
|
||||
if err != nil {
|
||||
return nil, internalError("creating user: " + err.Error())
|
||||
}
|
||||
|
||||
s.change(policyChanged)
|
||||
|
||||
return &oas.CreateUserOK{User: oas.NewOptUser(oasUser(user.View()))}, nil
|
||||
}
|
||||
|
||||
// ListUsers lists users, optionally filtered by id, name, or email, sorted by id.
|
||||
func (s *Server) ListUsers(
|
||||
_ context.Context,
|
||||
params oas.ListUsersParams,
|
||||
) (*oas.ListUsersOK, error) {
|
||||
var (
|
||||
users []types.User
|
||||
err error
|
||||
)
|
||||
|
||||
switch {
|
||||
case params.Name.Or("") != "":
|
||||
users, err = s.state.ListUsersWithFilter(&types.User{Name: params.Name.Or("")})
|
||||
case params.Email.Or("") != "":
|
||||
users, err = s.state.ListUsersWithFilter(&types.User{Email: params.Email.Or("")})
|
||||
case params.ID.Or(0) != 0:
|
||||
users, err = s.state.ListUsersWithFilter(
|
||||
&types.User{Model: gorm.Model{ID: uint(params.ID.Or(0))}},
|
||||
)
|
||||
default:
|
||||
users, err = s.state.ListAllUsers()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
slices.SortFunc(users, func(a, b types.User) int { return cmp.Compare(a.ID, b.ID) })
|
||||
|
||||
out := make([]oas.User, len(users))
|
||||
for i := range users {
|
||||
out[i] = oasUser(users[i].View())
|
||||
}
|
||||
|
||||
return &oas.ListUsersOK{Users: out}, nil
|
||||
}
|
||||
|
||||
// RenameUser renames a user and distributes the resulting policy change.
|
||||
func (s *Server) RenameUser(
|
||||
_ context.Context,
|
||||
params oas.RenameUserParams,
|
||||
) (*oas.RenameUserOK, error) {
|
||||
oldUser, err := s.state.GetUserByID(types.UserID(params.OldID))
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
_, c, err := s.state.RenameUser(types.UserID(oldUser.ID), params.NewName)
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
s.change(c)
|
||||
|
||||
newUser, err := s.state.GetUserByName(params.NewName)
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
return &oas.RenameUserOK{User: oas.NewOptUser(oasUser(newUser.View()))}, nil
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user and distributes the resulting policy change.
|
||||
func (s *Server) DeleteUser(_ context.Context, params oas.DeleteUserParams) error {
|
||||
user, err := s.state.GetUserByID(types.UserID(params.ID))
|
||||
if err != nil {
|
||||
return mapStateError(err)
|
||||
}
|
||||
|
||||
policyChanged, err := s.state.DeleteUser(types.UserID(user.ID))
|
||||
if err != nil {
|
||||
return mapStateError(err)
|
||||
}
|
||||
|
||||
s.change(policyChanged)
|
||||
|
||||
return nil
|
||||
}
|
||||
+53
-242
@@ -24,9 +24,8 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/metrics"
|
||||
grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/juanfont/headscale"
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
apiv1 "github.com/juanfont/headscale/hscontrol/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/capver"
|
||||
"github.com/juanfont/headscale/hscontrol/db"
|
||||
"github.com/juanfont/headscale/hscontrol/derp"
|
||||
@@ -37,22 +36,12 @@ import (
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/types/change"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
zerolog "github.com/philip-bui/grpc-zerolog"
|
||||
"github.com/pkg/profile"
|
||||
zl "github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"golang.org/x/crypto/acme"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/peer"
|
||||
"google.golang.org/grpc/reflection"
|
||||
"google.golang.org/grpc/status"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/dnstype"
|
||||
@@ -385,131 +374,6 @@ func (h *Headscale) scheduledTasks(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// checkBearerToken validates an "Authorization" header value. It reports
|
||||
// whether the API key is valid, whether the header carried the "Bearer "
|
||||
// prefix, and any validation error. Callers translate these outcomes into
|
||||
// their transport-specific status.
|
||||
func (h *Headscale) checkBearerToken(authHeader string) (bool, bool, error) {
|
||||
if !strings.HasPrefix(authHeader, AuthPrefix) {
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
valid, err := h.state.ValidateAPIKey(strings.TrimPrefix(authHeader, AuthPrefix))
|
||||
|
||||
return valid, true, err
|
||||
}
|
||||
|
||||
func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
|
||||
req any,
|
||||
info *grpc.UnaryServerInfo,
|
||||
handler grpc.UnaryHandler,
|
||||
) (any, error) {
|
||||
// Check if the request is coming from the on-server client.
|
||||
// This is not secure, but it is to maintain maintainability
|
||||
// with the "legacy" database-based client
|
||||
// It is also needed for grpc-gateway to be able to connect to
|
||||
// the server
|
||||
client, _ := peer.FromContext(ctx)
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("client_address", client.Addr.String()).
|
||||
Msg("Client is trying to authenticate")
|
||||
|
||||
meta, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return ctx, status.Errorf(
|
||||
codes.InvalidArgument,
|
||||
"retrieving metadata",
|
||||
)
|
||||
}
|
||||
|
||||
authHeader, ok := meta["authorization"]
|
||||
if !ok {
|
||||
return ctx, status.Errorf(
|
||||
codes.Unauthenticated,
|
||||
"authorization token not supplied",
|
||||
)
|
||||
}
|
||||
|
||||
valid, hasPrefix, err := h.checkBearerToken(authHeader[0])
|
||||
if !hasPrefix {
|
||||
return ctx, status.Error(
|
||||
codes.Unauthenticated,
|
||||
`missing "Bearer " prefix in "Authorization" header`,
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return ctx, status.Error(codes.Internal, "validating token")
|
||||
}
|
||||
|
||||
if !valid {
|
||||
log.Info().
|
||||
Str("client_address", client.Addr.String()).
|
||||
Msg("invalid token")
|
||||
|
||||
return ctx, status.Error(codes.Unauthenticated, "invalid token")
|
||||
}
|
||||
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
func (h *Headscale) httpAuthenticationMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(
|
||||
writer http.ResponseWriter,
|
||||
req *http.Request,
|
||||
) {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("client_address", req.RemoteAddr).
|
||||
Msg("HTTP authentication invoked")
|
||||
|
||||
authHeader := req.Header.Get("Authorization")
|
||||
|
||||
writeUnauthorized := func(statusCode int) {
|
||||
writer.WriteHeader(statusCode)
|
||||
|
||||
if _, err := writer.Write([]byte("Unauthorized")); err != nil { //nolint:noinlineerr
|
||||
log.Error().Err(err).Msg("writing HTTP response failed")
|
||||
}
|
||||
}
|
||||
|
||||
valid, hasPrefix, err := h.checkBearerToken(authHeader)
|
||||
if !hasPrefix {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("client_address", req.RemoteAddr).
|
||||
Msg(`missing "Bearer " prefix in "Authorization" header`)
|
||||
writeUnauthorized(http.StatusUnauthorized)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Info().
|
||||
Caller().
|
||||
Err(err).
|
||||
Str("client_address", req.RemoteAddr).
|
||||
Msg("failed to validate token")
|
||||
writeUnauthorized(http.StatusUnauthorized)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !valid {
|
||||
log.Info().
|
||||
Str("client_address", req.RemoteAddr).
|
||||
Msg("invalid token")
|
||||
writeUnauthorized(http.StatusUnauthorized)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(writer, req)
|
||||
})
|
||||
}
|
||||
|
||||
// ensureUnixSocketIsAbsent will check if the given path for headscales unix socket is clear
|
||||
// and will remove it if it is not.
|
||||
func (h *Headscale) ensureUnixSocketIsAbsent() error {
|
||||
@@ -535,7 +399,23 @@ func securityHeaders(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *chi.Mux {
|
||||
// unixSocketHandler wraps the API router for the local unix socket, marking
|
||||
// every request as socket-authenticated so the v1 API bypasses bearer-token
|
||||
// validation. The socket's filesystem permissions are the trust boundary.
|
||||
func unixSocketHandler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// ogen only invokes its security handler when an Authorization header
|
||||
// is present; inject a placeholder so socket requests reach it (where
|
||||
// the socket-auth marker then short-circuits validation).
|
||||
if r.Header.Get("Authorization") == "" {
|
||||
r.Header.Set("Authorization", "Bearer local-socket")
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(apiv1.WithSocketAuth(r.Context())))
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Headscale) createRouter(apiV1 http.Handler) *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
r.Use(metrics.Collector(metrics.CollectorOpts{
|
||||
Host: false,
|
||||
@@ -574,7 +454,7 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *chi.Mux {
|
||||
|
||||
// TODO(kristoffer): move swagger into a package
|
||||
r.Get("/swagger", headscale.SwaggerUI)
|
||||
r.Get("/swagger/v1/openapiv2.json", headscale.SwaggerAPIv1)
|
||||
r.Get("/swagger/v1/openapi.yaml", headscale.SwaggerAPIv1)
|
||||
|
||||
r.Post("/verify", h.VerifyHandler)
|
||||
|
||||
@@ -585,10 +465,11 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *chi.Mux {
|
||||
r.HandleFunc("/bootstrap-dns", derpServer.DERPBootstrapDNSHandler(h.state.DERPMap()))
|
||||
}
|
||||
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
r.Use(h.httpAuthenticationMiddleware)
|
||||
r.HandleFunc("/v1/*", grpcMux.ServeHTTP)
|
||||
})
|
||||
// v1 API (ogen). The generated server authenticates requests itself via
|
||||
// its SecurityHandler (bearer API key), so it is mounted outside the legacy
|
||||
// auth middleware. chi leaves r.URL.Path intact, so ogen's router sees the
|
||||
// full /api/v1/... path.
|
||||
r.Handle("/api/v1/*", apiV1)
|
||||
// Ping response endpoint: receives HEAD from clients responding
|
||||
// to a [tailcfg.PingRequest]. The unguessable ping ID serves as authentication.
|
||||
r.Head("/machine/ping-response", h.PingResponseHandler)
|
||||
@@ -599,7 +480,7 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *chi.Mux {
|
||||
return r
|
||||
}
|
||||
|
||||
// Serve launches the HTTP and gRPC server service Headscale and the API.
|
||||
// Serve launches the HTTP server serving Headscale and the v1 API.
|
||||
//
|
||||
//nolint:gocyclo // complex server startup function
|
||||
func (h *Headscale) Serve() error {
|
||||
@@ -690,12 +571,6 @@ func (h *Headscale) Serve() error {
|
||||
|
||||
go h.scheduledTasks(scheduleCtx)
|
||||
|
||||
if zl.GlobalLevel() == zl.TraceLevel {
|
||||
zerolog.RespLog = true
|
||||
} else {
|
||||
zerolog.RespLog = false
|
||||
}
|
||||
|
||||
// Prepare group for running listeners
|
||||
errorGroup := new(errgroup.Group)
|
||||
|
||||
@@ -723,45 +598,31 @@ func (h *Headscale) Serve() error {
|
||||
|
||||
socketListener, err := new(net.ListenConfig).Listen(context.Background(), "unix", h.cfg.UnixSocket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up gRPC socket: %w", err)
|
||||
return fmt.Errorf("setting up unix socket: %w", err)
|
||||
}
|
||||
|
||||
// Change socket permissions
|
||||
if err := os.Chmod(h.cfg.UnixSocket, h.cfg.UnixSocketPermission); err != nil { //nolint:noinlineerr
|
||||
return fmt.Errorf("changing gRPC socket permission: %w", err)
|
||||
return fmt.Errorf("changing unix socket permission: %w", err)
|
||||
}
|
||||
|
||||
grpcGatewayMux := grpcRuntime.NewServeMux()
|
||||
|
||||
// Make the grpc-gateway connect to grpc over socket
|
||||
grpcGatewayConn, err := grpc.Dial( //nolint:staticcheck // SA1019: deprecated but supported in 1.x
|
||||
h.cfg.UnixSocket,
|
||||
[]grpc.DialOption{
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithContextDialer(util.GrpcSocketDialer),
|
||||
}...,
|
||||
)
|
||||
// Build the v1 API handler and HTTP router once; both the local unix
|
||||
// socket and the public HTTP listener serve it.
|
||||
apiV1Handler, err := apiv1.NewHandler(h.state, h.cfg, h.Change)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up gRPC gateway via socket: %w", err)
|
||||
return fmt.Errorf("building v1 API handler: %w", err)
|
||||
}
|
||||
|
||||
// Connect to the gRPC server over localhost to skip
|
||||
// the authentication.
|
||||
err = v1.RegisterHeadscaleServiceHandler(ctx, grpcGatewayMux, grpcGatewayConn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("registering Headscale API service to gRPC: %w", err)
|
||||
router := h.createRouter(apiV1Handler)
|
||||
|
||||
// Serve the API over the local unix socket without bearer auth: the
|
||||
// socket's filesystem permissions are the trust boundary.
|
||||
socketHTTPServer := &http.Server{
|
||||
Handler: unixSocketHandler(router),
|
||||
ReadTimeout: types.HTTPTimeout,
|
||||
}
|
||||
|
||||
// Start the local gRPC server without TLS and without authentication
|
||||
grpcSocket := grpc.NewServer(
|
||||
// Uncomment to debug grpc communication.
|
||||
// zerolog.UnaryInterceptor(),
|
||||
)
|
||||
|
||||
v1.RegisterHeadscaleServiceServer(grpcSocket, newHeadscaleV1APIServer(h))
|
||||
reflection.Register(grpcSocket)
|
||||
|
||||
errorGroup.Go(func() error { return grpcSocket.Serve(socketListener) })
|
||||
errorGroup.Go(func() error { return socketHTTPServer.Serve(socketListener) })
|
||||
|
||||
//
|
||||
//
|
||||
@@ -773,66 +634,12 @@ func (h *Headscale) Serve() error {
|
||||
return fmt.Errorf("configuring TLS settings: %w", err)
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// gRPC setup
|
||||
//
|
||||
|
||||
// We are sadly not able to run gRPC and HTTPS (2.0) on the same
|
||||
// port because the connection mux does not support matching them
|
||||
// since they are so similar. There is multiple issues open and we
|
||||
// can revisit this if changes:
|
||||
// https://github.com/soheilhy/cmux/issues/68
|
||||
// https://github.com/soheilhy/cmux/issues/91
|
||||
|
||||
var (
|
||||
grpcServer *grpc.Server
|
||||
grpcListener net.Listener
|
||||
)
|
||||
|
||||
if tlsConfig != nil || h.cfg.GRPCAllowInsecure {
|
||||
log.Info().Msgf("enabling remote gRPC at %s", h.cfg.GRPCAddr)
|
||||
|
||||
grpcOptions := []grpc.ServerOption{
|
||||
grpc.ChainUnaryInterceptor(
|
||||
h.grpcAuthenticationInterceptor,
|
||||
// Uncomment to debug grpc communication.
|
||||
// zerolog.NewUnaryServerInterceptor(),
|
||||
),
|
||||
}
|
||||
|
||||
if tlsConfig != nil {
|
||||
grpcOptions = append(
|
||||
grpcOptions,
|
||||
grpc.Creds(credentials.NewTLS(tlsConfig)),
|
||||
)
|
||||
} else {
|
||||
log.Warn().Msg("gRPC is running without security")
|
||||
}
|
||||
|
||||
grpcServer = grpc.NewServer(grpcOptions...)
|
||||
|
||||
v1.RegisterHeadscaleServiceServer(grpcServer, newHeadscaleV1APIServer(h))
|
||||
|
||||
grpcListener, err = new(net.ListenConfig).Listen(context.Background(), "tcp", h.cfg.GRPCAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("binding to TCP address: %w", err)
|
||||
}
|
||||
|
||||
errorGroup.Go(func() error { return grpcServer.Serve(grpcListener) })
|
||||
|
||||
log.Info().
|
||||
Msgf("listening and serving gRPC on: %s", h.cfg.GRPCAddr)
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// HTTP setup
|
||||
//
|
||||
// This is the regular router that we expose
|
||||
// over our main Addr
|
||||
router := h.createRouter(grpcGatewayMux)
|
||||
|
||||
// This is the regular router that we expose over our main Addr; it is the
|
||||
// same router served over the unix socket above.
|
||||
httpServer := &http.Server{
|
||||
Addr: h.cfg.Addr,
|
||||
Handler: router,
|
||||
@@ -972,13 +779,11 @@ func (h *Headscale) Serve() error {
|
||||
info("waiting for netmap stream to close")
|
||||
h.clientStreamsOpen.Wait()
|
||||
|
||||
info("shutting down grpc server (socket)")
|
||||
grpcSocket.GracefulStop()
|
||||
info("shutting down socket http server")
|
||||
|
||||
if grpcServer != nil {
|
||||
info("shutting down grpc server (external)")
|
||||
grpcServer.GracefulStop()
|
||||
grpcListener.Close()
|
||||
socketErr := socketHTTPServer.Close()
|
||||
if socketErr != nil {
|
||||
log.Error().Err(socketErr).Msg("failed to shutdown socket http")
|
||||
}
|
||||
|
||||
if tailsqlContext != nil {
|
||||
@@ -994,7 +799,6 @@ func (h *Headscale) Serve() error {
|
||||
}
|
||||
|
||||
httpListener.Close()
|
||||
grpcGatewayConn.Close()
|
||||
|
||||
// Stop listening (and unlink the socket if unix type):
|
||||
info("closing socket listener")
|
||||
@@ -1158,7 +962,14 @@ func (h *Headscale) Change(cs ...change.Change) {
|
||||
// The handler serves the Tailscale control protocol including the /key
|
||||
// endpoint and /ts2021 Noise upgrade path.
|
||||
func (h *Headscale) HTTPHandler() http.Handler {
|
||||
return h.createRouter(grpcRuntime.NewServeMux())
|
||||
apiV1, err := apiv1.NewHandler(h.state, h.cfg, h.Change)
|
||||
if err != nil {
|
||||
// NewServer only fails on a nil handler or security handler, which
|
||||
// cannot happen here; treat it as a programming error.
|
||||
panic(fmt.Sprintf("building v1 API handler: %v", err))
|
||||
}
|
||||
|
||||
return h.createRouter(apiV1)
|
||||
}
|
||||
|
||||
// NoisePublicKey returns the server's Noise protocol public key.
|
||||
|
||||
@@ -179,9 +179,10 @@ func TestReAuthDoesNotReapplyTags(t *testing.T) {
|
||||
assert.Equal(t, 1, allNodes.Len(), "Should have exactly one node")
|
||||
}
|
||||
|
||||
// NOTE: TestSetTagsOnUserOwnedNode functionality is covered by gRPC tests in grpcv1_test.go
|
||||
// which properly handle ACL policy setup. The test verifies that [headscaleV1APIServer.SetTags] can convert
|
||||
// user-owned nodes to tagged nodes while preserving UserID.
|
||||
// NOTE: SetTags functionality is covered by the HTTP-API tests in
|
||||
// hscontrol/servertest/apiv1_nodes_test.go, which handle ACL policy setup and
|
||||
// verify that SetTags converts user-owned nodes to tagged nodes while
|
||||
// preserving UserID.
|
||||
|
||||
// TestCannotRemoveAllTags tests that attempting to remove all tags from a
|
||||
// tagged node fails with ErrCannotRemoveAllTags. Once a node is tagged,
|
||||
|
||||
@@ -4052,7 +4052,7 @@ func TestHandleNodeFromPreAuthKey_OldUserNil_NoPanic(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestHandleNodeFromAuthPath_OldUserNil_NoPanic is the parallel guard
|
||||
// for the gRPC/OIDC entry point. Same orphan shape as
|
||||
// for the OIDC/auth-path entry point. Same orphan shape as
|
||||
// TestHandleNodeFromPreAuthKey_OldUserNil_NoPanic; HandleNodeFromAuthPath
|
||||
// has its own oldUser.Name() log line in the existingNodeOwnedByOtherUser
|
||||
// branch and panics independently of the noise registration path.
|
||||
|
||||
@@ -309,9 +309,13 @@ func DestroyPreAuthKey(tx *gorm.DB, id uint64) error {
|
||||
}
|
||||
|
||||
// Then delete the pre-auth key
|
||||
err = tx.Unscoped().Delete(&types.PreAuthKey{}, id).Error
|
||||
if err != nil {
|
||||
return err
|
||||
res := tx.Unscoped().Delete(&types.PreAuthKey{}, id)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
|
||||
if res.RowsAffected == 0 {
|
||||
return ErrPreAuthKeyNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -356,5 +360,15 @@ func UsePreAuthKey(tx *gorm.DB, k *types.PreAuthKey) error {
|
||||
// ExpirePreAuthKey marks a [types.PreAuthKey] as expired.
|
||||
func ExpirePreAuthKey(tx *gorm.DB, id uint64) error {
|
||||
now := time.Now()
|
||||
return tx.Model(&types.PreAuthKey{}).Where("id = ?", id).Update("expiration", now).Error
|
||||
|
||||
res := tx.Model(&types.PreAuthKey{}).Where("id = ?", id).Update("expiration", now)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
|
||||
if res.RowsAffected == 0 {
|
||||
return ErrPreAuthKeyNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestPreAuthKeyACLTags(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listedPaks, 1)
|
||||
|
||||
gotTags := listedPaks[0].Proto().GetAclTags()
|
||||
gotTags := listedPaks[0].Tags
|
||||
slices.Sort(gotTags)
|
||||
assert.Equal(t, expectedTags, gotTags)
|
||||
},
|
||||
|
||||
@@ -1,944 +0,0 @@
|
||||
//go:generate buf generate --template ../buf.gen.yaml -o .. ../proto
|
||||
|
||||
// nolint
|
||||
package hscontrol
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/views"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/state"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
|
||||
)
|
||||
|
||||
type headscaleV1APIServer struct { // v1.HeadscaleServiceServer
|
||||
v1.UnimplementedHeadscaleServiceServer
|
||||
h *Headscale
|
||||
}
|
||||
|
||||
func newHeadscaleV1APIServer(h *Headscale) v1.HeadscaleServiceServer {
|
||||
return headscaleV1APIServer{
|
||||
h: h,
|
||||
}
|
||||
}
|
||||
|
||||
// sortByID sorts a slice of proto messages by ascending Id.
|
||||
func sortByID[T interface{ GetId() uint64 }](s []T) {
|
||||
slices.SortFunc(s, func(a, b T) int {
|
||||
return cmp.Compare(a.GetId(), b.GetId())
|
||||
})
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) CreateUser(
|
||||
ctx context.Context,
|
||||
request *v1.CreateUserRequest,
|
||||
) (*v1.CreateUserResponse, error) {
|
||||
newUser := types.User{
|
||||
Name: request.GetName(),
|
||||
DisplayName: request.GetDisplayName(),
|
||||
Email: request.GetEmail(),
|
||||
ProfilePicURL: request.GetPictureUrl(),
|
||||
}
|
||||
user, policyChanged, err := api.h.state.CreateUser(newUser)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "creating user: %s", err)
|
||||
}
|
||||
|
||||
// [state.State.CreateUser] returns a policy change response if the user creation affected policy.
|
||||
// This triggers a full policy re-evaluation for all connected nodes.
|
||||
api.h.Change(policyChanged)
|
||||
|
||||
return &v1.CreateUserResponse{User: user.Proto()}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) RenameUser(
|
||||
ctx context.Context,
|
||||
request *v1.RenameUserRequest,
|
||||
) (*v1.RenameUserResponse, error) {
|
||||
oldUser, err := api.h.state.GetUserByID(types.UserID(request.GetOldId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, c, err := api.h.state.RenameUser(types.UserID(oldUser.ID), request.GetNewName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Send policy update notifications if needed
|
||||
api.h.Change(c)
|
||||
|
||||
newUser, err := api.h.state.GetUserByName(request.GetNewName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v1.RenameUserResponse{User: newUser.Proto()}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) DeleteUser(
|
||||
ctx context.Context,
|
||||
request *v1.DeleteUserRequest,
|
||||
) (*v1.DeleteUserResponse, error) {
|
||||
user, err := api.h.state.GetUserByID(types.UserID(request.GetId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policyChanged, err := api.h.state.DeleteUser(types.UserID(user.ID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use the change returned from [state.State.DeleteUser] which includes proper policy updates
|
||||
api.h.Change(policyChanged)
|
||||
|
||||
return &v1.DeleteUserResponse{}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) ListUsers(
|
||||
ctx context.Context,
|
||||
request *v1.ListUsersRequest,
|
||||
) (*v1.ListUsersResponse, error) {
|
||||
var err error
|
||||
var users []types.User
|
||||
|
||||
switch {
|
||||
case request.GetName() != "":
|
||||
users, err = api.h.state.ListUsersWithFilter(&types.User{Name: request.GetName()})
|
||||
case request.GetEmail() != "":
|
||||
users, err = api.h.state.ListUsersWithFilter(&types.User{Email: request.GetEmail()})
|
||||
case request.GetId() != 0:
|
||||
users, err = api.h.state.ListUsersWithFilter(&types.User{Model: gorm.Model{ID: uint(request.GetId())}})
|
||||
default:
|
||||
users, err = api.h.state.ListAllUsers()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := make([]*v1.User, len(users))
|
||||
for index, user := range users {
|
||||
response[index] = user.Proto()
|
||||
}
|
||||
|
||||
sortByID(response)
|
||||
|
||||
return &v1.ListUsersResponse{Users: response}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) CreatePreAuthKey(
|
||||
ctx context.Context,
|
||||
request *v1.CreatePreAuthKeyRequest,
|
||||
) (*v1.CreatePreAuthKeyResponse, error) {
|
||||
var expiration time.Time
|
||||
if request.GetExpiration() != nil {
|
||||
expiration = request.GetExpiration().AsTime()
|
||||
}
|
||||
|
||||
for _, tag := range request.AclTags {
|
||||
err := validateTag(tag)
|
||||
if err != nil {
|
||||
return &v1.CreatePreAuthKeyResponse{
|
||||
PreAuthKey: nil,
|
||||
}, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
var userID *types.UserID
|
||||
if request.GetUser() != 0 {
|
||||
user, err := api.h.state.GetUserByID(types.UserID(request.GetUser()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userID = user.TypedID()
|
||||
}
|
||||
|
||||
preAuthKey, err := api.h.state.CreatePreAuthKey(
|
||||
userID,
|
||||
request.GetReusable(),
|
||||
request.GetEphemeral(),
|
||||
&expiration,
|
||||
request.AclTags,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v1.CreatePreAuthKeyResponse{PreAuthKey: preAuthKey.Proto()}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) ExpirePreAuthKey(
|
||||
ctx context.Context,
|
||||
request *v1.ExpirePreAuthKeyRequest,
|
||||
) (*v1.ExpirePreAuthKeyResponse, error) {
|
||||
err := api.h.state.ExpirePreAuthKey(request.GetId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v1.ExpirePreAuthKeyResponse{}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) DeletePreAuthKey(
|
||||
ctx context.Context,
|
||||
request *v1.DeletePreAuthKeyRequest,
|
||||
) (*v1.DeletePreAuthKeyResponse, error) {
|
||||
err := api.h.state.DeletePreAuthKey(request.GetId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v1.DeletePreAuthKeyResponse{}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) ListPreAuthKeys(
|
||||
ctx context.Context,
|
||||
request *v1.ListPreAuthKeysRequest,
|
||||
) (*v1.ListPreAuthKeysResponse, error) {
|
||||
preAuthKeys, err := api.h.state.ListPreAuthKeys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := make([]*v1.PreAuthKey, len(preAuthKeys))
|
||||
for index, key := range preAuthKeys {
|
||||
response[index] = key.Proto()
|
||||
}
|
||||
|
||||
sortByID(response)
|
||||
|
||||
return &v1.ListPreAuthKeysResponse{PreAuthKeys: response}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) RegisterNode(
|
||||
ctx context.Context,
|
||||
request *v1.RegisterNodeRequest,
|
||||
) (*v1.RegisterNodeResponse, error) {
|
||||
// Generate ephemeral registration key for tracking this registration flow in logs
|
||||
registrationKey, err := util.GenerateRegistrationKey()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to generate registration key")
|
||||
registrationKey = "" // Continue without key if generation fails
|
||||
}
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str(zf.UserName, request.GetUser()).
|
||||
Str(zf.RegistrationID, request.GetKey()).
|
||||
Str(zf.RegistrationKey, registrationKey).
|
||||
Msg("registering node")
|
||||
|
||||
registrationId, err := types.AuthIDFromString(request.GetKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := api.h.state.GetUserByName(request.GetUser())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("looking up user: %w", err)
|
||||
}
|
||||
|
||||
node, nodeChange, err := api.h.state.HandleNodeFromAuthPath(
|
||||
registrationId,
|
||||
types.UserID(user.ID),
|
||||
nil,
|
||||
util.RegisterMethodCLI,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str(zf.RegistrationKey, registrationKey).
|
||||
Err(err).
|
||||
Msg("failed to register node")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str(zf.RegistrationKey, registrationKey).
|
||||
EmbedObject(node).
|
||||
Msg("node registered successfully")
|
||||
|
||||
// This is a bit of a back and forth, but we have a bit of a chicken and egg
|
||||
// dependency here.
|
||||
// Because the way the policy manager works, we need to have the node
|
||||
// in the database, then add it to the policy manager and then we can
|
||||
// approve the route. This means we get this dance where the node is
|
||||
// first added to the database, then we add it to the policy manager via
|
||||
// SaveNode (which automatically updates the policy manager) and then we can auto approve the routes.
|
||||
// As that only approves the struct object, we need to save it again and
|
||||
// ensure we send an update.
|
||||
// This works, but might be another good candidate for doing some sort of
|
||||
// eventbus.
|
||||
routeChange, err := api.h.state.AutoApproveRoutes(node)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("auto approving routes: %w", err)
|
||||
}
|
||||
|
||||
// Send both changes. Empty changes are ignored by [Headscale.Change].
|
||||
api.h.Change(nodeChange, routeChange)
|
||||
|
||||
return &v1.RegisterNodeResponse{Node: node.Proto()}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) GetNode(
|
||||
ctx context.Context,
|
||||
request *v1.GetNodeRequest,
|
||||
) (*v1.GetNodeResponse, error) {
|
||||
node, ok := api.h.state.GetNodeByID(types.NodeID(request.GetNodeId()))
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.NotFound, "node not found")
|
||||
}
|
||||
|
||||
resp := node.Proto()
|
||||
|
||||
return &v1.GetNodeResponse{Node: resp}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) SetTags(
|
||||
ctx context.Context,
|
||||
request *v1.SetTagsRequest,
|
||||
) (*v1.SetTagsResponse, error) {
|
||||
// Validate tags not empty - tagged nodes must have at least one tag
|
||||
if len(request.GetTags()) == 0 {
|
||||
return &v1.SetTagsResponse{
|
||||
Node: nil,
|
||||
}, status.Error(
|
||||
codes.InvalidArgument,
|
||||
"cannot remove all tags from a node - tagged nodes must have at least one tag",
|
||||
)
|
||||
}
|
||||
|
||||
// Validate tag format
|
||||
for _, tag := range request.GetTags() {
|
||||
err := validateTag(tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// User XOR Tags: nodes are either tagged or user-owned, never both.
|
||||
// Setting tags on a user-owned node converts it to a tagged node.
|
||||
// Once tagged, a node cannot be converted back to user-owned.
|
||||
_, found := api.h.state.GetNodeByID(types.NodeID(request.GetNodeId()))
|
||||
if !found {
|
||||
return &v1.SetTagsResponse{
|
||||
Node: nil,
|
||||
}, status.Error(codes.NotFound, "node not found")
|
||||
}
|
||||
|
||||
node, nodeChange, err := api.h.state.SetNodeTags(types.NodeID(request.GetNodeId()), request.GetTags())
|
||||
if err != nil {
|
||||
return &v1.SetTagsResponse{
|
||||
Node: nil,
|
||||
}, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
api.h.Change(nodeChange)
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
EmbedObject(node).
|
||||
Strs("tags", request.GetTags()).
|
||||
Msg("changing tags of node")
|
||||
|
||||
return &v1.SetTagsResponse{Node: node.Proto()}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) SetApprovedRoutes(
|
||||
ctx context.Context,
|
||||
request *v1.SetApprovedRoutesRequest,
|
||||
) (*v1.SetApprovedRoutesResponse, error) {
|
||||
log.Debug().
|
||||
Caller().
|
||||
Uint64(zf.NodeID, request.GetNodeId()).
|
||||
Strs("requestedRoutes", request.GetRoutes()).
|
||||
Msg("gRPC SetApprovedRoutes called")
|
||||
|
||||
var newApproved []netip.Prefix
|
||||
for _, route := range request.GetRoutes() {
|
||||
prefix, err := netip.ParsePrefix(route)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing route: %w", err)
|
||||
}
|
||||
|
||||
// If the prefix is an exit route, add both. The client expect both
|
||||
// to annotate the node as an exit node.
|
||||
if prefix == tsaddr.AllIPv4() || prefix == tsaddr.AllIPv6() {
|
||||
newApproved = append(newApproved, tsaddr.AllIPv4(), tsaddr.AllIPv6())
|
||||
} else {
|
||||
newApproved = append(newApproved, prefix)
|
||||
}
|
||||
}
|
||||
slices.SortFunc(newApproved, netip.Prefix.Compare)
|
||||
newApproved = slices.Compact(newApproved)
|
||||
|
||||
node, nodeChange, err := api.h.state.SetApprovedRoutes(types.NodeID(request.GetNodeId()), newApproved)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
// Always propagate node changes from [state.State.SetApprovedRoutes]
|
||||
api.h.Change(nodeChange)
|
||||
|
||||
proto := node.Proto()
|
||||
// Populate [types.Node.SubnetRoutes] with [tailcfg.Node.PrimaryRoutes] to ensure it includes only the
|
||||
// routes that are actively served from the node (per architectural requirement in types/node.go)
|
||||
primaryRoutes := api.h.state.GetNodePrimaryRoutes(node.ID())
|
||||
proto.SubnetRoutes = util.PrefixesToString(primaryRoutes)
|
||||
|
||||
log.Debug().
|
||||
Caller().
|
||||
EmbedObject(node).
|
||||
Strs("approvedRoutes", util.PrefixesToString(node.ApprovedRoutes().AsSlice())).
|
||||
Strs("primaryRoutes", util.PrefixesToString(primaryRoutes)).
|
||||
Strs("finalSubnetRoutes", proto.SubnetRoutes).
|
||||
Msg("gRPC SetApprovedRoutes completed")
|
||||
|
||||
return &v1.SetApprovedRoutesResponse{Node: proto}, nil
|
||||
}
|
||||
|
||||
func validateTag(tag string) error {
|
||||
if !strings.HasPrefix(tag, "tag:") {
|
||||
return errors.New("tag must start with the string 'tag:'")
|
||||
}
|
||||
if strings.ToLower(tag) != tag {
|
||||
return errors.New("tag should be lowercase")
|
||||
}
|
||||
if len(strings.Fields(tag)) > 1 {
|
||||
return errors.New("tags must not contain spaces")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) DeleteNode(
|
||||
ctx context.Context,
|
||||
request *v1.DeleteNodeRequest,
|
||||
) (*v1.DeleteNodeResponse, error) {
|
||||
node, ok := api.h.state.GetNodeByID(types.NodeID(request.GetNodeId()))
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.NotFound, "node not found")
|
||||
}
|
||||
|
||||
nodeChange, err := api.h.state.DeleteNode(node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api.h.Change(nodeChange)
|
||||
|
||||
return &v1.DeleteNodeResponse{}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) ExpireNode(
|
||||
ctx context.Context,
|
||||
request *v1.ExpireNodeRequest,
|
||||
) (*v1.ExpireNodeResponse, error) {
|
||||
if request.GetDisableExpiry() && request.GetExpiry() != nil {
|
||||
return nil, status.Error(
|
||||
codes.InvalidArgument,
|
||||
"cannot set both disable_expiry and expiry",
|
||||
)
|
||||
}
|
||||
|
||||
// Handle disable expiry request - node will never expire.
|
||||
if request.GetDisableExpiry() {
|
||||
node, nodeChange, err := api.h.state.SetNodeExpiry(
|
||||
types.NodeID(request.GetNodeId()), nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api.h.Change(nodeChange)
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
EmbedObject(node).
|
||||
Msg("node expiry disabled")
|
||||
|
||||
return &v1.ExpireNodeResponse{Node: node.Proto()}, nil
|
||||
}
|
||||
|
||||
expiry := time.Now()
|
||||
if request.GetExpiry() != nil {
|
||||
expiry = request.GetExpiry().AsTime()
|
||||
}
|
||||
|
||||
node, nodeChange, err := api.h.state.SetNodeExpiry(
|
||||
types.NodeID(request.GetNodeId()), &expiry,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(kradalby): Ensure that both the selfupdate and peer updates are sent
|
||||
api.h.Change(nodeChange)
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
EmbedObject(node).
|
||||
Time(zf.ExpiresAt, expiry).
|
||||
Msg("node expired")
|
||||
|
||||
return &v1.ExpireNodeResponse{Node: node.Proto()}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) RenameNode(
|
||||
ctx context.Context,
|
||||
request *v1.RenameNodeRequest,
|
||||
) (*v1.RenameNodeResponse, error) {
|
||||
node, nodeChange, err := api.h.state.RenameNode(types.NodeID(request.GetNodeId()), request.GetNewName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(kradalby): investigate if we need selfupdate
|
||||
api.h.Change(nodeChange)
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
EmbedObject(node).
|
||||
Str(zf.NewName, request.GetNewName()).
|
||||
Msg("node renamed")
|
||||
|
||||
return &v1.RenameNodeResponse{Node: node.Proto()}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) ListNodes(
|
||||
ctx context.Context,
|
||||
request *v1.ListNodesRequest,
|
||||
) (*v1.ListNodesResponse, error) {
|
||||
// TODO(kradalby): This should be done in one tx.
|
||||
var nodes views.Slice[types.NodeView]
|
||||
if request.GetUser() != "" {
|
||||
user, err := api.h.state.GetUserByName(request.GetUser())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodes = api.h.state.ListNodesByUser(types.UserID(user.ID))
|
||||
} else {
|
||||
nodes = api.h.state.ListNodes()
|
||||
}
|
||||
|
||||
response := nodesToProto(api.h.state, nodes)
|
||||
return &v1.ListNodesResponse{Nodes: response}, nil
|
||||
}
|
||||
|
||||
func nodesToProto(state *state.State, nodes views.Slice[types.NodeView]) []*v1.Node {
|
||||
response := make([]*v1.Node, nodes.Len())
|
||||
for index, node := range nodes.All() {
|
||||
resp := node.Proto()
|
||||
|
||||
// Tags-as-identity: tagged nodes show as [types.TaggedDevices] user in API responses
|
||||
// (UserID may be set internally for "created by" tracking)
|
||||
if node.IsTagged() {
|
||||
resp.User = types.TaggedDevices.Proto()
|
||||
}
|
||||
|
||||
resp.SubnetRoutes = util.PrefixesToString(append(state.GetNodePrimaryRoutes(node.ID()), node.ExitRoutes()...))
|
||||
response[index] = resp
|
||||
}
|
||||
|
||||
sortByID(response)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) BackfillNodeIPs(
|
||||
ctx context.Context,
|
||||
request *v1.BackfillNodeIPsRequest,
|
||||
) (*v1.BackfillNodeIPsResponse, error) {
|
||||
log.Trace().Caller().Msg("backfill called")
|
||||
|
||||
if !request.Confirmed {
|
||||
return nil, errors.New("not confirmed, aborting")
|
||||
}
|
||||
|
||||
changes, err := api.h.state.BackfillNodeIPs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v1.BackfillNodeIPsResponse{Changes: changes}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) CreateApiKey(
|
||||
ctx context.Context,
|
||||
request *v1.CreateApiKeyRequest,
|
||||
) (*v1.CreateApiKeyResponse, error) {
|
||||
var expiration time.Time
|
||||
if request.GetExpiration() != nil {
|
||||
expiration = request.GetExpiration().AsTime()
|
||||
}
|
||||
|
||||
apiKey, _, err := api.h.state.CreateAPIKey(&expiration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v1.CreateApiKeyResponse{ApiKey: apiKey}, nil
|
||||
}
|
||||
|
||||
// apiKeyIdentifier is implemented by requests that identify an API key.
|
||||
type apiKeyIdentifier interface {
|
||||
GetId() uint64
|
||||
GetPrefix() string
|
||||
}
|
||||
|
||||
// getAPIKey retrieves an API key by ID or prefix from the request.
|
||||
// Returns InvalidArgument if neither or both are provided.
|
||||
func (api headscaleV1APIServer) getAPIKey(req apiKeyIdentifier) (*types.APIKey, error) {
|
||||
hasID := req.GetId() != 0
|
||||
hasPrefix := req.GetPrefix() != ""
|
||||
|
||||
switch {
|
||||
case hasID && hasPrefix:
|
||||
return nil, status.Error(codes.InvalidArgument, "provide either id or prefix, not both")
|
||||
case hasID:
|
||||
return api.h.state.GetAPIKeyByID(req.GetId())
|
||||
case hasPrefix:
|
||||
return api.h.state.GetAPIKey(req.GetPrefix())
|
||||
default:
|
||||
return nil, status.Error(codes.InvalidArgument, "must provide id or prefix")
|
||||
}
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) ExpireApiKey(
|
||||
ctx context.Context,
|
||||
request *v1.ExpireApiKeyRequest,
|
||||
) (*v1.ExpireApiKeyResponse, error) {
|
||||
apiKey, err := api.getAPIKey(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = api.h.state.ExpireAPIKey(apiKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v1.ExpireApiKeyResponse{}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) ListApiKeys(
|
||||
ctx context.Context,
|
||||
request *v1.ListApiKeysRequest,
|
||||
) (*v1.ListApiKeysResponse, error) {
|
||||
apiKeys, err := api.h.state.ListAPIKeys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := make([]*v1.ApiKey, len(apiKeys))
|
||||
for index, key := range apiKeys {
|
||||
response[index] = key.Proto()
|
||||
}
|
||||
|
||||
sortByID(response)
|
||||
|
||||
return &v1.ListApiKeysResponse{ApiKeys: response}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) DeleteApiKey(
|
||||
ctx context.Context,
|
||||
request *v1.DeleteApiKeyRequest,
|
||||
) (*v1.DeleteApiKeyResponse, error) {
|
||||
apiKey, err := api.getAPIKey(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := api.h.state.DestroyAPIKey(*apiKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v1.DeleteApiKeyResponse{}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) GetPolicy(
|
||||
_ context.Context,
|
||||
_ *v1.GetPolicyRequest,
|
||||
) (*v1.GetPolicyResponse, error) {
|
||||
switch api.h.cfg.Policy.Mode {
|
||||
case types.PolicyModeDB:
|
||||
p, err := api.h.state.GetPolicy()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading ACL from database: %w", err)
|
||||
}
|
||||
|
||||
return &v1.GetPolicyResponse{
|
||||
Policy: p.Data,
|
||||
UpdatedAt: timestamppb.New(p.UpdatedAt),
|
||||
}, nil
|
||||
case types.PolicyModeFile:
|
||||
// Read the file and return the contents as-is.
|
||||
absPath := util.AbsolutePathFromConfigPath(api.h.cfg.Policy.Path)
|
||||
f, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading policy from path %q: %w", absPath, err)
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading policy from file: %w", err)
|
||||
}
|
||||
|
||||
return &v1.GetPolicyResponse{Policy: string(b)}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no supported policy mode found in configuration, policy.mode: %q", api.h.cfg.Policy.Mode)
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) SetPolicy(
|
||||
_ context.Context,
|
||||
request *v1.SetPolicyRequest,
|
||||
) (*v1.SetPolicyResponse, error) {
|
||||
if api.h.cfg.Policy.Mode != types.PolicyModeDB {
|
||||
return nil, types.ErrPolicyUpdateIsDisabled
|
||||
}
|
||||
|
||||
p := request.GetPolicy()
|
||||
|
||||
// Validate and reject configuration that would error when applied
|
||||
// when creating a map response. This requires nodes, so there is still
|
||||
// a scenario where they might be allowed if the server has no nodes
|
||||
// yet, but it should help for the general case and for hot reloading
|
||||
// configurations.
|
||||
nodes := api.h.state.ListNodes()
|
||||
|
||||
_, err := api.h.state.SetPolicy([]byte(p))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting policy: %w", err)
|
||||
}
|
||||
|
||||
if nodes.Len() > 0 {
|
||||
_, err = api.h.state.SSHPolicy(nodes.At(0))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("verifying SSH rules: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := api.h.state.SetPolicyInDB(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Always reload policy to ensure route re-evaluation, even if policy content hasn't changed.
|
||||
// This ensures that routes are re-evaluated for auto-approval in cases where routes
|
||||
// were manually disabled but could now be auto-approved with the current policy.
|
||||
cs, err := api.h.state.ReloadPolicy()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reloading policy: %w", err)
|
||||
}
|
||||
|
||||
if len(cs) > 0 {
|
||||
api.h.Change(cs...)
|
||||
} else {
|
||||
log.Debug().
|
||||
Caller().
|
||||
Msg("No policy changes to distribute because ReloadPolicy returned empty changeset")
|
||||
}
|
||||
|
||||
response := &v1.SetPolicyResponse{
|
||||
Policy: updated.Data,
|
||||
UpdatedAt: timestamppb.New(updated.UpdatedAt),
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Caller().
|
||||
Msg("gRPC SetPolicy completed successfully because response prepared")
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// CheckPolicy validates the given policy against the server's live users
|
||||
// and nodes, running its `tests` block as a sandbox. Nothing is persisted
|
||||
// and the live PolicyManager is not touched. Works regardless of
|
||||
// policy.mode so operators can validate a policy file before storing it.
|
||||
func (api headscaleV1APIServer) CheckPolicy(
|
||||
_ context.Context,
|
||||
request *v1.CheckPolicyRequest,
|
||||
) (*v1.CheckPolicyResponse, error) {
|
||||
polB := []byte(request.GetPolicy())
|
||||
|
||||
users, err := api.h.state.ListAllUsers()
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "loading users: %s", err)
|
||||
}
|
||||
|
||||
nodes := api.h.state.ListNodes()
|
||||
|
||||
pm, err := policyv2.NewPolicyManager(polB, users, nodes)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
if _, err := pm.SetPolicy(polB); err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
return &v1.CheckPolicyResponse{}, nil
|
||||
}
|
||||
|
||||
// The following service calls are for testing and debugging
|
||||
func (api headscaleV1APIServer) DebugCreateNode(
|
||||
ctx context.Context,
|
||||
request *v1.DebugCreateNodeRequest,
|
||||
) (*v1.DebugCreateNodeResponse, error) {
|
||||
user, err := api.h.state.GetUserByName(request.GetUser())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes, err := util.StringToIPPrefix(request.GetRoutes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
Interface("route-prefix", routes).
|
||||
Interface("route-str", request.GetRoutes()).
|
||||
Msg("Creating routes for node")
|
||||
|
||||
registrationId, err := types.AuthIDFromString(request.GetKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
regData := &types.RegistrationData{
|
||||
NodeKey: key.NewNode().Public(),
|
||||
MachineKey: key.NewMachine().Public(),
|
||||
Hostname: request.GetName(),
|
||||
Expiry: &time.Time{}, // zero time, not nil — preserves proto JSON round-trip semantics
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Caller().
|
||||
Str("registration_id", registrationId.String()).
|
||||
Msg("adding debug machine via CLI, appending to registration cache")
|
||||
|
||||
authRegReq := types.NewRegisterAuthRequest(regData)
|
||||
api.h.state.SetAuthCacheEntry(registrationId, authRegReq)
|
||||
|
||||
// Echo back a synthetic [types.Node] so the debug response surface stays
|
||||
// stable. The actual node is created later by [headscaleV1APIServer.AuthApprove] via
|
||||
// [state.State.HandleNodeFromAuthPath] using the cached [types.RegistrationData].
|
||||
echoNode := types.Node{
|
||||
NodeKey: regData.NodeKey,
|
||||
MachineKey: regData.MachineKey,
|
||||
Hostname: regData.Hostname,
|
||||
User: user,
|
||||
Expiry: &time.Time{},
|
||||
LastSeen: &time.Time{},
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: request.GetName(),
|
||||
OS: "TestOS",
|
||||
RoutableIPs: routes,
|
||||
},
|
||||
}
|
||||
|
||||
return &v1.DebugCreateNodeResponse{Node: echoNode.Proto()}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) Health(
|
||||
ctx context.Context,
|
||||
request *v1.HealthRequest,
|
||||
) (*v1.HealthResponse, error) {
|
||||
var healthErr error
|
||||
response := &v1.HealthResponse{}
|
||||
|
||||
if err := api.h.state.PingDB(ctx); err != nil {
|
||||
healthErr = fmt.Errorf("pinging database: %w", err)
|
||||
} else {
|
||||
response.DatabaseConnectivity = true
|
||||
}
|
||||
|
||||
if healthErr != nil {
|
||||
log.Error().Err(healthErr).Msg("health check failed")
|
||||
}
|
||||
|
||||
return response, healthErr
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) AuthRegister(
|
||||
ctx context.Context,
|
||||
request *v1.AuthRegisterRequest,
|
||||
) (*v1.AuthRegisterResponse, error) {
|
||||
resp, err := api.RegisterNode(ctx, &v1.RegisterNodeRequest{
|
||||
Key: request.GetAuthId(),
|
||||
User: request.GetUser(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v1.AuthRegisterResponse{Node: resp.GetNode()}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) AuthApprove(
|
||||
ctx context.Context,
|
||||
request *v1.AuthApproveRequest,
|
||||
) (*v1.AuthApproveResponse, error) {
|
||||
authID, err := types.AuthIDFromString(request.GetAuthId())
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid auth_id: %v", err)
|
||||
}
|
||||
|
||||
authReq, ok := api.h.state.GetAuthCacheEntry(authID)
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.NotFound, "no pending auth session for auth_id %s", authID)
|
||||
}
|
||||
|
||||
authReq.FinishAuth(types.AuthVerdict{})
|
||||
|
||||
return &v1.AuthApproveResponse{}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) AuthReject(
|
||||
ctx context.Context,
|
||||
request *v1.AuthRejectRequest,
|
||||
) (*v1.AuthRejectResponse, error) {
|
||||
authID, err := types.AuthIDFromString(request.GetAuthId())
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid auth_id: %v", err)
|
||||
}
|
||||
|
||||
authReq, ok := api.h.state.GetAuthCacheEntry(authID)
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.NotFound, "no pending auth session for auth_id %s", authID)
|
||||
}
|
||||
|
||||
authReq.FinishAuth(types.AuthVerdict{
|
||||
Err: errors.New("auth request rejected"),
|
||||
})
|
||||
|
||||
return &v1.AuthRejectResponse{}, nil
|
||||
}
|
||||
|
||||
func (api headscaleV1APIServer) mustEmbedUnimplementedHeadscaleServiceServer() {}
|
||||
@@ -1,819 +0,0 @@
|
||||
package hscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func Test_validateTag(t *testing.T) {
|
||||
type args struct {
|
||||
tag string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid tag",
|
||||
args: args{tag: "tag:test"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "tag without tag prefix",
|
||||
args: args{tag: "test"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "uppercase tag",
|
||||
args: args{tag: "tag:tEST"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "tag that contains space",
|
||||
args: args{tag: "tag:this is a spaced tag"},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateTag(tt.args.tag)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("validateTag() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetTags_Conversion tests the conversion of user-owned nodes to tagged nodes.
|
||||
// The tags-as-identity model allows one-way conversion from user-owned to tagged.
|
||||
// Tag authorization is checked via the policy manager - unauthorized tags are rejected.
|
||||
func TestSetTags_Conversion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
|
||||
// Create test user and nodes
|
||||
user := app.state.CreateUserForTest("test-user")
|
||||
|
||||
// Create a pre-auth key WITHOUT tags for user-owned node
|
||||
pak, err := app.state.CreatePreAuthKey(user.TypedID(), false, false, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
machineKey1 := key.NewMachine()
|
||||
nodeKey1 := key.NewNode()
|
||||
|
||||
// Register a user-owned node (via untagged PreAuthKey)
|
||||
userOwnedReq := tailcfg.RegisterRequest{
|
||||
Auth: &tailcfg.RegisterResponseAuth{
|
||||
AuthKey: pak.Key,
|
||||
},
|
||||
NodeKey: nodeKey1.Public(),
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: "user-owned-node",
|
||||
},
|
||||
}
|
||||
_, err = app.handleRegisterWithAuthKey(userOwnedReq, machineKey1.Public())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get the created node
|
||||
userOwnedNode, found := app.state.GetNodeByNodeKey(nodeKey1.Public())
|
||||
require.True(t, found)
|
||||
|
||||
// Create API server instance
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
nodeID uint64
|
||||
tags []string
|
||||
wantErr bool
|
||||
wantCode codes.Code
|
||||
wantErrMessage string
|
||||
}{
|
||||
{
|
||||
// Conversion is allowed, but tag authorization fails without tagOwners
|
||||
name: "reject unauthorized tags on user-owned node",
|
||||
nodeID: uint64(userOwnedNode.ID()),
|
||||
tags: []string{"tag:server"},
|
||||
wantErr: true,
|
||||
wantCode: codes.InvalidArgument,
|
||||
wantErrMessage: "requested tags",
|
||||
},
|
||||
{
|
||||
// Conversion is allowed, but tag authorization fails without tagOwners
|
||||
name: "reject multiple unauthorized tags",
|
||||
nodeID: uint64(userOwnedNode.ID()),
|
||||
tags: []string{"tag:server", "tag:database"},
|
||||
wantErr: true,
|
||||
wantCode: codes.InvalidArgument,
|
||||
wantErrMessage: "requested tags",
|
||||
},
|
||||
{
|
||||
name: "reject non-existent node",
|
||||
nodeID: 99999,
|
||||
tags: []string{"tag:server"},
|
||||
wantErr: true,
|
||||
wantCode: codes.NotFound,
|
||||
wantErrMessage: "node not found",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
resp, err := apiServer.SetTags(context.Background(), &v1.SetTagsRequest{
|
||||
NodeId: tt.nodeID,
|
||||
Tags: tt.tags,
|
||||
})
|
||||
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
st, ok := status.FromError(err)
|
||||
require.True(t, ok, "error should be a gRPC status error")
|
||||
assert.Equal(t, tt.wantCode, st.Code())
|
||||
assert.Contains(t, st.Message(), tt.wantErrMessage)
|
||||
assert.Nil(t, resp.GetNode())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.NotNil(t, resp.GetNode())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetTags_TaggedNode tests that [headscaleV1APIServer.SetTags] correctly identifies tagged nodes
|
||||
// and doesn't reject them with the "user-owned nodes" error.
|
||||
// Note: This test doesn't validate ACL tag authorization - that's tested elsewhere.
|
||||
func TestSetTags_TaggedNode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
|
||||
// Create test user and tagged pre-auth key
|
||||
user := app.state.CreateUserForTest("test-user")
|
||||
pak, err := app.state.CreatePreAuthKey(user.TypedID(), false, false, nil, []string{"tag:initial"})
|
||||
require.NoError(t, err)
|
||||
|
||||
machineKey := key.NewMachine()
|
||||
nodeKey := key.NewNode()
|
||||
|
||||
// Register a tagged node (via tagged PreAuthKey)
|
||||
taggedReq := tailcfg.RegisterRequest{
|
||||
Auth: &tailcfg.RegisterResponseAuth{
|
||||
AuthKey: pak.Key,
|
||||
},
|
||||
NodeKey: nodeKey.Public(),
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: "tagged-node",
|
||||
},
|
||||
}
|
||||
_, err = app.handleRegisterWithAuthKey(taggedReq, machineKey.Public())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get the created node
|
||||
taggedNode, found := app.state.GetNodeByNodeKey(nodeKey.Public())
|
||||
require.True(t, found)
|
||||
assert.True(t, taggedNode.IsTagged(), "Node should be tagged")
|
||||
assert.False(t, taggedNode.UserID().Valid(), "Tagged node should not have UserID")
|
||||
|
||||
// Create API server instance
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
|
||||
// Test: [headscaleV1APIServer.SetTags] should work on tagged nodes.
|
||||
resp, err := apiServer.SetTags(context.Background(), &v1.SetTagsRequest{
|
||||
NodeId: uint64(taggedNode.ID()),
|
||||
Tags: []string{"tag:initial"}, // Keep existing tag to avoid ACL validation issues
|
||||
})
|
||||
|
||||
// The call should NOT fail with "cannot set tags on user-owned nodes"
|
||||
if err != nil {
|
||||
st, ok := status.FromError(err)
|
||||
require.True(t, ok)
|
||||
// If error is about unauthorized tags, that's fine - ACL validation is working
|
||||
// If error is about user-owned nodes, that's the bug we're testing for
|
||||
assert.NotContains(t, st.Message(), "user-owned nodes", "Should not reject tagged nodes as user-owned")
|
||||
} else {
|
||||
// Success is also fine
|
||||
assert.NotNil(t, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetTags_CannotRemoveAllTags tests that [headscaleV1APIServer.SetTags] rejects attempts to remove
|
||||
// all tags from a tagged node, enforcing Tailscale's requirement that tagged
|
||||
// nodes must have at least one tag.
|
||||
func TestSetTags_CannotRemoveAllTags(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
|
||||
// Create test user and tagged pre-auth key
|
||||
user := app.state.CreateUserForTest("test-user")
|
||||
pak, err := app.state.CreatePreAuthKey(user.TypedID(), false, false, nil, []string{"tag:server"})
|
||||
require.NoError(t, err)
|
||||
|
||||
machineKey := key.NewMachine()
|
||||
nodeKey := key.NewNode()
|
||||
|
||||
// Register a tagged node
|
||||
taggedReq := tailcfg.RegisterRequest{
|
||||
Auth: &tailcfg.RegisterResponseAuth{
|
||||
AuthKey: pak.Key,
|
||||
},
|
||||
NodeKey: nodeKey.Public(),
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: "tagged-node",
|
||||
},
|
||||
}
|
||||
_, err = app.handleRegisterWithAuthKey(taggedReq, machineKey.Public())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get the created node
|
||||
taggedNode, found := app.state.GetNodeByNodeKey(nodeKey.Public())
|
||||
require.True(t, found)
|
||||
assert.True(t, taggedNode.IsTagged())
|
||||
|
||||
// Create API server instance
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
|
||||
// Attempt to remove all tags (empty array)
|
||||
resp, err := apiServer.SetTags(context.Background(), &v1.SetTagsRequest{
|
||||
NodeId: uint64(taggedNode.ID()),
|
||||
Tags: []string{}, // Empty - attempting to remove all tags
|
||||
})
|
||||
|
||||
// Should fail with InvalidArgument error
|
||||
require.Error(t, err)
|
||||
st, ok := status.FromError(err)
|
||||
require.True(t, ok, "error should be a gRPC status error")
|
||||
assert.Equal(t, codes.InvalidArgument, st.Code())
|
||||
assert.Contains(t, st.Message(), "cannot remove all tags")
|
||||
assert.Nil(t, resp.GetNode())
|
||||
}
|
||||
|
||||
// TestSetTags_ClearsUserIDInDatabase tests that converting a user-owned node
|
||||
// to a tagged node via [headscaleV1APIServer.SetTags] correctly persists user_id = NULL in the
|
||||
// database, not just in-memory.
|
||||
func TestSetTags_ClearsUserIDInDatabase(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
|
||||
user := app.state.CreateUserForTest("tag-owner")
|
||||
err := app.state.UpdatePolicyManagerUsersForTest()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = app.state.SetPolicy([]byte(`{
|
||||
"tagOwners": {"tag:server": ["tag-owner@"]},
|
||||
"acls": [{"action": "accept", "src": ["*"], "dst": ["*:*"]}]
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Register a user-owned node (untagged PreAuthKey).
|
||||
pak, err := app.state.CreatePreAuthKey(user.TypedID(), false, false, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
machineKey := key.NewMachine()
|
||||
nodeKey := key.NewNode()
|
||||
|
||||
regReq := tailcfg.RegisterRequest{
|
||||
Auth: &tailcfg.RegisterResponseAuth{
|
||||
AuthKey: pak.Key,
|
||||
},
|
||||
NodeKey: nodeKey.Public(),
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: "user-owned-node",
|
||||
},
|
||||
}
|
||||
_, err = app.handleRegisterWithAuthKey(regReq, machineKey.Public())
|
||||
require.NoError(t, err)
|
||||
|
||||
node, found := app.state.GetNodeByNodeKey(nodeKey.Public())
|
||||
require.True(t, found)
|
||||
require.False(t, node.IsTagged(), "node should start as user-owned")
|
||||
require.True(t, node.UserID().Valid(), "user-owned node must have UserID")
|
||||
|
||||
nodeID := node.ID()
|
||||
|
||||
// Convert to tagged via [headscaleV1APIServer.SetTags] API.
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
_, err = apiServer.SetTags(context.Background(), &v1.SetTagsRequest{
|
||||
NodeId: uint64(nodeID),
|
||||
Tags: []string{"tag:server"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify in-memory state is correct.
|
||||
nsNode, found := app.state.GetNodeByID(nodeID)
|
||||
require.True(t, found)
|
||||
assert.True(t, nsNode.IsTagged(), "NodeStore: node should be tagged")
|
||||
assert.False(t, nsNode.UserID().Valid(),
|
||||
"NodeStore: UserID should be nil for tagged node")
|
||||
|
||||
// THE CRITICAL CHECK: verify database has user_id = NULL.
|
||||
dbNode, err := app.state.DB().GetNodeByID(nodeID)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, dbNode.UserID,
|
||||
"Database: user_id must be NULL after converting to tagged node")
|
||||
assert.True(t, dbNode.IsTagged(),
|
||||
"Database: tags must be set")
|
||||
}
|
||||
|
||||
// TestSetTags_NodeDisappearsFromUserListing tests issue #3161:
|
||||
// after converting a user-owned node to tagged, it must no longer appear
|
||||
// when listing nodes filtered by the original user.
|
||||
// https://github.com/juanfont/headscale/issues/3161
|
||||
func TestSetTags_NodeDisappearsFromUserListing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
|
||||
user := app.state.CreateUserForTest("list-user")
|
||||
err := app.state.UpdatePolicyManagerUsersForTest()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = app.state.SetPolicy([]byte(`{
|
||||
"tagOwners": {"tag:web": ["list-user@"]},
|
||||
"acls": [{"action": "accept", "src": ["*"], "dst": ["*:*"]}]
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Register a user-owned node.
|
||||
pak, err := app.state.CreatePreAuthKey(user.TypedID(), false, false, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
machineKey := key.NewMachine()
|
||||
nodeKey := key.NewNode()
|
||||
|
||||
regReq := tailcfg.RegisterRequest{
|
||||
Auth: &tailcfg.RegisterResponseAuth{
|
||||
AuthKey: pak.Key,
|
||||
},
|
||||
NodeKey: nodeKey.Public(),
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: "web-server",
|
||||
},
|
||||
}
|
||||
_, err = app.handleRegisterWithAuthKey(regReq, machineKey.Public())
|
||||
require.NoError(t, err)
|
||||
|
||||
node, found := app.state.GetNodeByNodeKey(nodeKey.Public())
|
||||
require.True(t, found)
|
||||
|
||||
// Verify node appears under user before tagging.
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
resp, err := apiServer.ListNodes(context.Background(), &v1.ListNodesRequest{
|
||||
User: "list-user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, resp.GetNodes(), 1, "user-owned node should appear under user")
|
||||
|
||||
// Convert to tagged.
|
||||
_, err = apiServer.SetTags(context.Background(), &v1.SetTagsRequest{
|
||||
NodeId: uint64(node.ID()),
|
||||
Tags: []string{"tag:web"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Node must NOT appear when listing by original user.
|
||||
resp, err = apiServer.ListNodes(context.Background(), &v1.ListNodesRequest{
|
||||
User: "list-user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, resp.GetNodes(),
|
||||
"tagged node must not appear when listing nodes for original user")
|
||||
|
||||
// Node must still appear in unfiltered listing.
|
||||
allResp, err := apiServer.ListNodes(context.Background(), &v1.ListNodesRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, allResp.GetNodes(), 1)
|
||||
assert.Contains(t, allResp.GetNodes()[0].GetTags(), "tag:web")
|
||||
}
|
||||
|
||||
// TestSetTags_NodeStoreAndDBConsistency verifies that after [headscaleV1APIServer.SetTags], the
|
||||
// in-memory [state.NodeStore] and the database agree on the node's ownership state.
|
||||
func TestSetTags_NodeStoreAndDBConsistency(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
|
||||
user := app.state.CreateUserForTest("consistency-user")
|
||||
err := app.state.UpdatePolicyManagerUsersForTest()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = app.state.SetPolicy([]byte(`{
|
||||
"tagOwners": {"tag:db": ["consistency-user@"]},
|
||||
"acls": [{"action": "accept", "src": ["*"], "dst": ["*:*"]}]
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
pak, err := app.state.CreatePreAuthKey(user.TypedID(), false, false, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
machineKey := key.NewMachine()
|
||||
nodeKey := key.NewNode()
|
||||
|
||||
regReq := tailcfg.RegisterRequest{
|
||||
Auth: &tailcfg.RegisterResponseAuth{
|
||||
AuthKey: pak.Key,
|
||||
},
|
||||
NodeKey: nodeKey.Public(),
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: "db-node",
|
||||
},
|
||||
}
|
||||
_, err = app.handleRegisterWithAuthKey(regReq, machineKey.Public())
|
||||
require.NoError(t, err)
|
||||
|
||||
node, found := app.state.GetNodeByNodeKey(nodeKey.Public())
|
||||
require.True(t, found)
|
||||
|
||||
nodeID := node.ID()
|
||||
|
||||
// Convert to tagged.
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
_, err = apiServer.SetTags(context.Background(), &v1.SetTagsRequest{
|
||||
NodeId: uint64(nodeID),
|
||||
Tags: []string{"tag:db"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// In-memory state.
|
||||
nsNode, found := app.state.GetNodeByID(nodeID)
|
||||
require.True(t, found)
|
||||
|
||||
// Database state.
|
||||
dbNode, err := app.state.DB().GetNodeByID(nodeID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Both must agree: tagged, no UserID.
|
||||
assert.True(t, nsNode.IsTagged(), "NodeStore: should be tagged")
|
||||
assert.True(t, dbNode.IsTagged(), "Database: should be tagged")
|
||||
|
||||
assert.False(t, nsNode.UserID().Valid(),
|
||||
"NodeStore: UserID should be nil")
|
||||
assert.Nil(t, dbNode.UserID,
|
||||
"Database: user_id should be NULL")
|
||||
|
||||
assert.Equal(t,
|
||||
nsNode.UserID().Valid(),
|
||||
dbNode.UserID != nil,
|
||||
"NodeStore and database must agree on UserID state")
|
||||
}
|
||||
|
||||
// TestSetTags_UserDeletionDoesNotCascadeToTaggedNode tests that deleting the
|
||||
// original user does not cascade-delete a node that was converted to tagged
|
||||
// via [headscaleV1APIServer.SetTags]. This catches the real-world consequence of stale user_id:
|
||||
// ON DELETE CASCADE would destroy the tagged node.
|
||||
func TestSetTags_UserDeletionDoesNotCascadeToTaggedNode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
|
||||
user := app.state.CreateUserForTest("doomed-user")
|
||||
err := app.state.UpdatePolicyManagerUsersForTest()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = app.state.SetPolicy([]byte(`{
|
||||
"tagOwners": {"tag:survivor": ["doomed-user@"]},
|
||||
"acls": [{"action": "accept", "src": ["*"], "dst": ["*:*"]}]
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
pak, err := app.state.CreatePreAuthKey(user.TypedID(), false, false, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
machineKey := key.NewMachine()
|
||||
nodeKey := key.NewNode()
|
||||
|
||||
regReq := tailcfg.RegisterRequest{
|
||||
Auth: &tailcfg.RegisterResponseAuth{
|
||||
AuthKey: pak.Key,
|
||||
},
|
||||
NodeKey: nodeKey.Public(),
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: "survivor-node",
|
||||
},
|
||||
}
|
||||
_, err = app.handleRegisterWithAuthKey(regReq, machineKey.Public())
|
||||
require.NoError(t, err)
|
||||
|
||||
node, found := app.state.GetNodeByNodeKey(nodeKey.Public())
|
||||
require.True(t, found)
|
||||
|
||||
nodeID := node.ID()
|
||||
|
||||
// Convert to tagged.
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
_, err = apiServer.SetTags(context.Background(), &v1.SetTagsRequest{
|
||||
NodeId: uint64(nodeID),
|
||||
Tags: []string{"tag:survivor"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Delete the original user.
|
||||
_, err = app.state.DeleteUser(*user.TypedID())
|
||||
require.NoError(t, err)
|
||||
|
||||
// The tagged node must survive in both [state.NodeStore] and database.
|
||||
nsNode, found := app.state.GetNodeByID(nodeID)
|
||||
require.True(t, found, "tagged node must survive user deletion in NodeStore")
|
||||
assert.True(t, nsNode.IsTagged())
|
||||
|
||||
dbNode, err := app.state.DB().GetNodeByID(nodeID)
|
||||
require.NoError(t, err, "tagged node must survive user deletion in database")
|
||||
assert.True(t, dbNode.IsTagged())
|
||||
assert.Nil(t, dbNode.UserID)
|
||||
}
|
||||
|
||||
// TestDeleteUser_ReturnsProperChangeSignal tests issue #2967 fix:
|
||||
// When a user is deleted, the state should return a non-empty change signal
|
||||
// to ensure policy manager is updated and clients are notified immediately.
|
||||
func TestDeleteUser_ReturnsProperChangeSignal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
|
||||
// Create a user
|
||||
user := app.state.CreateUserForTest("test-user-to-delete")
|
||||
require.NotNil(t, user)
|
||||
|
||||
// Delete the user and verify a non-empty change is returned
|
||||
// Without the fix, [state.State.DeleteUser] returned an empty change,
|
||||
// causing stale policy state until another user operation triggered an update.
|
||||
changeSignal, err := app.state.DeleteUser(*user.TypedID())
|
||||
require.NoError(t, err, "DeleteUser should succeed")
|
||||
assert.False(t, changeSignal.IsEmpty(), "DeleteUser should return a non-empty change signal (issue #2967)")
|
||||
}
|
||||
|
||||
// TestDeleteUser_TaggedNodeSurvives tests that deleting a user succeeds when
|
||||
// the user's only nodes are tagged, and that those nodes remain in the
|
||||
// [state.NodeStore] with nil UserID.
|
||||
func TestDeleteUser_TaggedNodeSurvives(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
|
||||
user := app.state.CreateUserForTest("legacy-user")
|
||||
|
||||
// Register a tagged node via the full auth flow.
|
||||
tags := []string{"tag:server"}
|
||||
pak, err := app.state.CreatePreAuthKey(user.TypedID(), true, false, nil, tags)
|
||||
require.NoError(t, err)
|
||||
|
||||
machineKey := key.NewMachine()
|
||||
nodeKey := key.NewNode()
|
||||
|
||||
regReq := tailcfg.RegisterRequest{
|
||||
Auth: &tailcfg.RegisterResponseAuth{
|
||||
AuthKey: pak.Key,
|
||||
},
|
||||
NodeKey: nodeKey.Public(),
|
||||
Hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: "tagged-server",
|
||||
},
|
||||
Expiry: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
|
||||
resp, err := app.handleRegisterWithAuthKey(regReq, machineKey.Public())
|
||||
require.NoError(t, err)
|
||||
require.True(t, resp.MachineAuthorized)
|
||||
|
||||
// Verify the registered node has nil UserID (enforced at registration).
|
||||
node, found := app.state.GetNodeByNodeKey(nodeKey.Public())
|
||||
require.True(t, found)
|
||||
require.True(t, node.IsTagged())
|
||||
assert.False(t, node.UserID().Valid(),
|
||||
"tagged node should have nil UserID after registration")
|
||||
|
||||
nodeID := node.ID()
|
||||
|
||||
// [state.NodeStore] should not list the tagged node under any user.
|
||||
nodesForUser := app.state.ListNodesByUser(types.UserID(user.ID))
|
||||
assert.Equal(t, 0, nodesForUser.Len(),
|
||||
"tagged nodes should not appear in nodesByUser index")
|
||||
|
||||
// Delete the user.
|
||||
changeSignal, err := app.state.DeleteUser(*user.TypedID())
|
||||
require.NoError(t, err)
|
||||
assert.False(t, changeSignal.IsEmpty())
|
||||
|
||||
// Tagged node survives in the [state.NodeStore].
|
||||
nodeAfter, found := app.state.GetNodeByID(nodeID)
|
||||
require.True(t, found, "tagged node should survive user deletion")
|
||||
assert.True(t, nodeAfter.IsTagged())
|
||||
assert.False(t, nodeAfter.UserID().Valid())
|
||||
|
||||
// Tagged node appears in the global list.
|
||||
allNodes := app.state.ListNodes()
|
||||
foundInAll := false
|
||||
|
||||
for _, n := range allNodes.All() {
|
||||
if n.ID() == nodeID {
|
||||
foundInAll = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, foundInAll, "tagged node should appear in the global node list")
|
||||
}
|
||||
|
||||
// TestExpireApiKey_ByID tests that API keys can be expired by ID.
|
||||
func TestExpireApiKey_ByID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
|
||||
// Create an API key
|
||||
createResp, err := apiServer.CreateApiKey(context.Background(), &v1.CreateApiKeyRequest{})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, createResp.GetApiKey())
|
||||
|
||||
// List keys to get the ID
|
||||
listResp, err := apiServer.ListApiKeys(context.Background(), &v1.ListApiKeysRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listResp.GetApiKeys(), 1)
|
||||
|
||||
keyID := listResp.GetApiKeys()[0].GetId()
|
||||
|
||||
// Expire by ID
|
||||
_, err = apiServer.ExpireApiKey(context.Background(), &v1.ExpireApiKeyRequest{
|
||||
Id: keyID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify key is expired (expiration is set to now or in the past)
|
||||
listResp, err = apiServer.ListApiKeys(context.Background(), &v1.ListApiKeysRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listResp.GetApiKeys(), 1)
|
||||
assert.NotNil(t, listResp.GetApiKeys()[0].GetExpiration(), "expiration should be set")
|
||||
}
|
||||
|
||||
// TestExpireApiKey_ByPrefix tests that API keys can still be expired by prefix.
|
||||
func TestExpireApiKey_ByPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
|
||||
// Create an API key
|
||||
createResp, err := apiServer.CreateApiKey(context.Background(), &v1.CreateApiKeyRequest{})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, createResp.GetApiKey())
|
||||
|
||||
// List keys to get the prefix
|
||||
listResp, err := apiServer.ListApiKeys(context.Background(), &v1.ListApiKeysRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listResp.GetApiKeys(), 1)
|
||||
|
||||
keyPrefix := listResp.GetApiKeys()[0].GetPrefix()
|
||||
|
||||
// Expire by prefix
|
||||
_, err = apiServer.ExpireApiKey(context.Background(), &v1.ExpireApiKeyRequest{
|
||||
Prefix: keyPrefix,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestDeleteApiKey_ByID tests that API keys can be deleted by ID.
|
||||
func TestDeleteApiKey_ByID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
|
||||
// Create an API key
|
||||
createResp, err := apiServer.CreateApiKey(context.Background(), &v1.CreateApiKeyRequest{})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, createResp.GetApiKey())
|
||||
|
||||
// List keys to get the ID
|
||||
listResp, err := apiServer.ListApiKeys(context.Background(), &v1.ListApiKeysRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listResp.GetApiKeys(), 1)
|
||||
|
||||
keyID := listResp.GetApiKeys()[0].GetId()
|
||||
|
||||
// Delete by ID
|
||||
_, err = apiServer.DeleteApiKey(context.Background(), &v1.DeleteApiKeyRequest{
|
||||
Id: keyID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify key is deleted
|
||||
listResp, err = apiServer.ListApiKeys(context.Background(), &v1.ListApiKeysRequest{})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, listResp.GetApiKeys())
|
||||
}
|
||||
|
||||
// TestDeleteApiKey_ByPrefix tests that API keys can still be deleted by prefix.
|
||||
func TestDeleteApiKey_ByPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
|
||||
// Create an API key
|
||||
createResp, err := apiServer.CreateApiKey(context.Background(), &v1.CreateApiKeyRequest{})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, createResp.GetApiKey())
|
||||
|
||||
// List keys to get the prefix
|
||||
listResp, err := apiServer.ListApiKeys(context.Background(), &v1.ListApiKeysRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listResp.GetApiKeys(), 1)
|
||||
|
||||
keyPrefix := listResp.GetApiKeys()[0].GetPrefix()
|
||||
|
||||
// Delete by prefix
|
||||
_, err = apiServer.DeleteApiKey(context.Background(), &v1.DeleteApiKeyRequest{
|
||||
Prefix: keyPrefix,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify key is deleted
|
||||
listResp, err = apiServer.ListApiKeys(context.Background(), &v1.ListApiKeysRequest{})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, listResp.GetApiKeys())
|
||||
}
|
||||
|
||||
// TestExpireApiKey_NoIdentifier tests that an error is returned when neither ID nor prefix is provided.
|
||||
func TestExpireApiKey_NoIdentifier(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
|
||||
_, err := apiServer.ExpireApiKey(context.Background(), &v1.ExpireApiKeyRequest{})
|
||||
require.Error(t, err)
|
||||
st, ok := status.FromError(err)
|
||||
require.True(t, ok, "error should be a gRPC status error")
|
||||
assert.Equal(t, codes.InvalidArgument, st.Code())
|
||||
assert.Contains(t, st.Message(), "must provide id or prefix")
|
||||
}
|
||||
|
||||
// TestDeleteApiKey_NoIdentifier tests that an error is returned when neither ID nor prefix is provided.
|
||||
func TestDeleteApiKey_NoIdentifier(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
|
||||
_, err := apiServer.DeleteApiKey(context.Background(), &v1.DeleteApiKeyRequest{})
|
||||
require.Error(t, err)
|
||||
st, ok := status.FromError(err)
|
||||
require.True(t, ok, "error should be a gRPC status error")
|
||||
assert.Equal(t, codes.InvalidArgument, st.Code())
|
||||
assert.Contains(t, st.Message(), "must provide id or prefix")
|
||||
}
|
||||
|
||||
// TestExpireApiKey_BothIdentifiers tests that an error is returned when both ID and prefix are provided.
|
||||
func TestExpireApiKey_BothIdentifiers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
|
||||
_, err := apiServer.ExpireApiKey(context.Background(), &v1.ExpireApiKeyRequest{
|
||||
Id: 1,
|
||||
Prefix: "test",
|
||||
})
|
||||
require.Error(t, err)
|
||||
st, ok := status.FromError(err)
|
||||
require.True(t, ok, "error should be a gRPC status error")
|
||||
assert.Equal(t, codes.InvalidArgument, st.Code())
|
||||
assert.Contains(t, st.Message(), "provide either id or prefix, not both")
|
||||
}
|
||||
|
||||
// TestDeleteApiKey_BothIdentifiers tests that an error is returned when both ID and prefix are provided.
|
||||
func TestDeleteApiKey_BothIdentifiers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
apiServer := newHeadscaleV1APIServer(app)
|
||||
|
||||
_, err := apiServer.DeleteApiKey(context.Background(), &v1.DeleteApiKeyRequest{
|
||||
Id: 1,
|
||||
Prefix: "test",
|
||||
})
|
||||
require.Error(t, err)
|
||||
st, ok := status.FromError(err)
|
||||
require.True(t, ok, "error should be a gRPC status error")
|
||||
assert.Equal(t, codes.InvalidArgument, st.Code())
|
||||
assert.Contains(t, st.Message(), "provide either id or prefix, not both")
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package servertest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
)
|
||||
|
||||
// APIClient returns an ogen-generated v1 API client wired to this server's
|
||||
// in-memory network and authenticated with apiKey (use [TestServer.CreateAPIKey]
|
||||
// to mint one). This is the entry point for HTTP-API parity tests: the
|
||||
// generated client talks to the generated server in-process, exercising the
|
||||
// real request/response encoding.
|
||||
func (s *TestServer) APIClient(tb testing.TB, apiKey string) *apiv1.Client {
|
||||
tb.Helper()
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return s.memNet.Dial(ctx, network, addr)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
client, err := apiv1.NewClient(
|
||||
s.URL,
|
||||
bearerToken(apiKey),
|
||||
apiv1.WithClient(httpClient),
|
||||
)
|
||||
if err != nil {
|
||||
tb.Fatalf("servertest: building API client: %v", err)
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// CreateNode creates a registered test node present in both the database and
|
||||
// the in-memory NodeStore, so it can be read and mutated through the API.
|
||||
func (s *TestServer) CreateNode(
|
||||
tb testing.TB,
|
||||
user *types.User,
|
||||
hostname string,
|
||||
) *types.Node {
|
||||
tb.Helper()
|
||||
|
||||
node := s.st.CreateRegisteredNodeForTest(user, hostname)
|
||||
// Ensure the User association is present in the NodeStore snapshot; the
|
||||
// database read path preloads it, but the test helper does not.
|
||||
node.User = user
|
||||
s.st.PutNodeInStoreForTest(*node)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
// CreateAPIKey mints a non-expiring API key and returns the secret token.
|
||||
func (s *TestServer) CreateAPIKey(tb testing.TB) string {
|
||||
tb.Helper()
|
||||
|
||||
expiry := time.Now().Add(24 * time.Hour)
|
||||
|
||||
key, _, err := s.st.CreateAPIKey(&expiry)
|
||||
if err != nil {
|
||||
tb.Fatalf("servertest: CreateAPIKey: %v", err)
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
// bearerToken is an [apiv1.SecuritySource] that supplies a fixed bearer token.
|
||||
type bearerToken string
|
||||
|
||||
func (t bearerToken) BearerAuth(
|
||||
context.Context,
|
||||
apiv1.OperationName,
|
||||
) (apiv1.BearerAuth, error) {
|
||||
return apiv1.BearerAuth{Token: string(t)}, nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package servertest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIv1_CreateApiKey(t *testing.T) {
|
||||
_, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
resp, err := client.CreateApiKey(ctx, &apiv1.CreateApiKeyReq{
|
||||
Expiration: apiv1.NewOptDateTime(time.Now().Add(time.Hour)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, resp.ApiKey.Value)
|
||||
}
|
||||
|
||||
func TestAPIv1_ListApiKeys(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
before, err := client.ListApiKeys(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
exp := time.Now().Add(time.Hour)
|
||||
_, _, err = srv.State().CreateAPIKey(&exp)
|
||||
require.NoError(t, err)
|
||||
|
||||
after, err := client.ListApiKeys(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, after.ApiKeys, len(before.ApiKeys)+1)
|
||||
|
||||
for i := 1; i < len(after.ApiKeys); i++ {
|
||||
assert.LessOrEqual(t, after.ApiKeys[i-1].ID.Value, after.ApiKeys[i].ID.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIv1_ExpireApiKey(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
exp := time.Now().Add(time.Hour)
|
||||
_, key, err := srv.State().CreateAPIKey(&exp)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, client.ExpireApiKey(ctx, &apiv1.ExpireApiKeyReq{
|
||||
ID: apiv1.NewOptUint64(key.ID),
|
||||
}))
|
||||
|
||||
got, err := srv.State().GetAPIKeyByID(key.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got.Expiration)
|
||||
assert.True(t, got.Expiration.Before(time.Now()), "key should be expired")
|
||||
|
||||
// Both id and prefix is a 400.
|
||||
requireProblem(t, client.ExpireApiKey(ctx, &apiv1.ExpireApiKeyReq{
|
||||
ID: apiv1.NewOptUint64(1),
|
||||
Prefix: apiv1.NewOptString("abc"),
|
||||
}), http.StatusBadRequest)
|
||||
|
||||
// Neither id nor prefix is a 400.
|
||||
requireProblem(t, client.ExpireApiKey(ctx, &apiv1.ExpireApiKeyReq{}), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestAPIv1_DeleteApiKey(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
exp := time.Now().Add(time.Hour)
|
||||
_, key, err := srv.State().CreateAPIKey(&exp)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, client.DeleteApiKey(ctx, apiv1.DeleteApiKeyParams{Prefix: key.Prefix}))
|
||||
|
||||
_, err = srv.State().GetAPIKeyByID(key.ID)
|
||||
require.Error(t, err, "key should be gone")
|
||||
|
||||
// Unknown prefix is a 404.
|
||||
requireProblem(t, client.DeleteApiKey(ctx, apiv1.DeleteApiKeyParams{Prefix: "nonexistent"}), http.StatusNotFound)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package servertest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIv1_AuthRegister_Errors(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
srv.CreateUser(t, "alice")
|
||||
|
||||
// Malformed auth id is a 400.
|
||||
_, err := client.AuthRegister(ctx, &apiv1.AuthRegisterReq{
|
||||
User: apiv1.NewOptString("alice"),
|
||||
AuthId: apiv1.NewOptString("not-valid"),
|
||||
})
|
||||
requireProblem(t, err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestAPIv1_AuthApprove(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
srv.CreateUser(t, "alice")
|
||||
|
||||
authID := types.MustAuthID()
|
||||
_, err := client.DebugCreateNode(ctx, &apiv1.DebugCreateNodeReq{
|
||||
User: apiv1.NewOptString("alice"),
|
||||
Key: apiv1.NewOptString(authID.String()),
|
||||
Name: apiv1.NewOptString("pending-node"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
authReq, ok := srv.State().GetAuthCacheEntry(authID)
|
||||
require.True(t, ok)
|
||||
|
||||
// FinishAuth sends on an unbuffered channel; drain it so AuthApprove returns.
|
||||
verdict := make(chan types.AuthVerdict, 1)
|
||||
go func() { verdict <- <-authReq.WaitForAuth() }()
|
||||
|
||||
require.NoError(t, client.AuthApprove(ctx, &apiv1.AuthApproveReq{
|
||||
AuthId: apiv1.NewOptString(authID.String()),
|
||||
}))
|
||||
assert.NoError(t, (<-verdict).Err, "approval verdict should carry no error")
|
||||
}
|
||||
|
||||
func TestAPIv1_AuthReject(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
srv.CreateUser(t, "alice")
|
||||
|
||||
authID := types.MustAuthID()
|
||||
_, err := client.DebugCreateNode(ctx, &apiv1.DebugCreateNodeReq{
|
||||
User: apiv1.NewOptString("alice"),
|
||||
Key: apiv1.NewOptString(authID.String()),
|
||||
Name: apiv1.NewOptString("pending-node"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
authReq, ok := srv.State().GetAuthCacheEntry(authID)
|
||||
require.True(t, ok)
|
||||
|
||||
verdict := make(chan types.AuthVerdict, 1)
|
||||
go func() { verdict <- <-authReq.WaitForAuth() }()
|
||||
|
||||
require.NoError(t, client.AuthReject(ctx, &apiv1.AuthRejectReq{
|
||||
AuthId: apiv1.NewOptString(authID.String()),
|
||||
}))
|
||||
assert.Error(t, (<-verdict).Err, "rejection verdict should carry an error")
|
||||
}
|
||||
|
||||
func TestAPIv1_AuthApprove_Errors(t *testing.T) {
|
||||
_, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Malformed auth id is a 400.
|
||||
requireProblem(t, client.AuthApprove(ctx, &apiv1.AuthApproveReq{
|
||||
AuthId: apiv1.NewOptString("not-valid"),
|
||||
}), http.StatusBadRequest)
|
||||
|
||||
// Unknown (but well-formed) auth id is a 404.
|
||||
requireProblem(t, client.AuthApprove(ctx, &apiv1.AuthApproveReq{
|
||||
AuthId: apiv1.NewOptString(types.MustAuthID().String()),
|
||||
}), http.StatusNotFound)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package servertest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
)
|
||||
|
||||
// These cases assert the HTTP error translation for paths that the
|
||||
// per-resource happy-path tests do not already cover; they are the ones that
|
||||
// caught the RenameNode/ExpireNode 500 and the pre-auth-key silent-success bugs.
|
||||
// Not-found cases that the per-resource tests already assert (DeleteNode,
|
||||
// DeleteUser, RenameUser, ExpireApiKey, DeleteApiKey) live there, not here.
|
||||
|
||||
func TestAPIv1_Nodes_NotFound(t *testing.T) {
|
||||
_, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
const missing = uint64(99999)
|
||||
|
||||
_, err := client.RenameNode(ctx, apiv1.RenameNodeParams{NodeID: missing, NewName: "x"})
|
||||
requireProblem(t, err, http.StatusNotFound)
|
||||
|
||||
_, err = client.ExpireNode(ctx, apiv1.ExpireNodeParams{NodeID: missing})
|
||||
requireProblem(t, err, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIv1_PreAuthKeys_Errors(t *testing.T) {
|
||||
_, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
requireProblem(t, client.ExpirePreAuthKey(ctx, &apiv1.ExpirePreAuthKeyReq{
|
||||
ID: apiv1.NewOptUint64(99999),
|
||||
}), http.StatusNotFound)
|
||||
|
||||
requireProblem(t, client.DeletePreAuthKey(ctx, apiv1.DeletePreAuthKeyParams{
|
||||
ID: apiv1.NewOptUint64(99999),
|
||||
}), http.StatusNotFound)
|
||||
|
||||
// Creating a key for a user that does not exist is a 404.
|
||||
_, err := client.CreatePreAuthKey(ctx, &apiv1.CreatePreAuthKeyReq{
|
||||
User: apiv1.NewOptUint64(99999),
|
||||
})
|
||||
requireProblem(t, err, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIv1_SetApprovedRoutes_InvalidCIDR(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
user := srv.CreateUser(t, "route-user")
|
||||
node := srv.CreateNode(t, user, "route-node")
|
||||
|
||||
_, err := client.SetApprovedRoutes(ctx,
|
||||
&apiv1.SetApprovedRoutesReq{Routes: []string{"not-a-cidr"}},
|
||||
apiv1.SetApprovedRoutesParams{NodeID: uint64(node.ID)},
|
||||
)
|
||||
requireProblem(t, err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestAPIv1_SetPolicy_Invalid(t *testing.T) {
|
||||
_, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := client.SetPolicy(ctx, &apiv1.SetPolicyReq{
|
||||
Policy: apiv1.NewOptString("{ this is not valid hujson"),
|
||||
})
|
||||
requireProblem(t, err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestAPIv1_CreatePreAuthKey_InvalidTag(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
user := srv.CreateUser(t, "tag-user")
|
||||
|
||||
_, err := client.CreatePreAuthKey(ctx, &apiv1.CreatePreAuthKeyReq{
|
||||
User: apiv1.NewOptUint64(uint64(user.ID)),
|
||||
AclTags: []string{"not-a-tag"},
|
||||
})
|
||||
requireProblem(t, err, http.StatusBadRequest)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package servertest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/servertest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestAPIv1_Health is the foundation smoke test: the ogen-generated client
|
||||
// talks to the ogen-generated server in-process and gets a healthy response
|
||||
// reporting database connectivity.
|
||||
func TestAPIv1_Health(t *testing.T) {
|
||||
_, client := apiClient(t)
|
||||
|
||||
resp, err := client.Health(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.True(
|
||||
t,
|
||||
resp.DatabaseConnectivity.Value,
|
||||
"expected database connectivity true",
|
||||
)
|
||||
}
|
||||
|
||||
// TestAPIv1_Health_Unauthorized verifies the bearer-auth SecurityHandler:
|
||||
// an invalid API key yields a 401 RFC 7807 problem, matching the previous
|
||||
// behaviour of rejecting bad tokens.
|
||||
func TestAPIv1_Health_Unauthorized(t *testing.T) {
|
||||
srv := servertest.NewServer(t)
|
||||
client := srv.APIClient(t, "tskey-invalid")
|
||||
|
||||
_, err := client.Health(context.Background())
|
||||
require.Error(t, err)
|
||||
|
||||
var problem *apiv1.ErrorStatusCode
|
||||
require.ErrorAs(
|
||||
t,
|
||||
err, &problem,
|
||||
"expected *apiv1.ErrorStatusCode, got %T",
|
||||
err,
|
||||
)
|
||||
assert.Equal(t, http.StatusUnauthorized, problem.StatusCode)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package servertest_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/servertest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// apiClient spins up a server and returns an authenticated v1 API client plus
|
||||
// the server, for HTTP-parity tests.
|
||||
func apiClient(t *testing.T) (*servertest.TestServer, *apiv1.Client) {
|
||||
t.Helper()
|
||||
|
||||
srv := servertest.NewServer(t)
|
||||
|
||||
return srv, srv.APIClient(t, srv.CreateAPIKey(t))
|
||||
}
|
||||
|
||||
// requireProblem asserts that err is an RFC 7807 problem with the given HTTP
|
||||
// status code.
|
||||
func requireProblem(t *testing.T, err error, status int) {
|
||||
t.Helper()
|
||||
|
||||
var problem *apiv1.ErrorStatusCode
|
||||
require.ErrorAsf(t, err, &problem, "expected *apiv1.ErrorStatusCode, got %T: %v", err, err)
|
||||
assert.Equal(t, status, problem.StatusCode)
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package servertest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIv1_GetNode(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
user := srv.CreateUser(t, "alice")
|
||||
node := srv.CreateNode(t, user, "node1")
|
||||
|
||||
resp, err := client.GetNode(ctx, apiv1.GetNodeParams{NodeID: uint64(node.ID)})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(node.ID), resp.Node.Value.ID.Value)
|
||||
assert.Equal(t, "alice", resp.Node.Value.User.Value.Name.Value)
|
||||
|
||||
_, err = client.GetNode(ctx, apiv1.GetNodeParams{NodeID: 99999})
|
||||
requireProblem(t, err, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIv1_ListNodes(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
alice := srv.CreateUser(t, "alice")
|
||||
bob := srv.CreateUser(t, "bob")
|
||||
srv.CreateNode(t, alice, "alice1")
|
||||
srv.CreateNode(t, alice, "alice2")
|
||||
srv.CreateNode(t, bob, "bob1")
|
||||
|
||||
all, err := client.ListNodes(ctx, apiv1.ListNodesParams{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, all.Nodes, 3)
|
||||
|
||||
for i := 1; i < len(all.Nodes); i++ {
|
||||
assert.Less(t, all.Nodes[i-1].ID.Value, all.Nodes[i].ID.Value)
|
||||
}
|
||||
|
||||
byUser, err := client.ListNodes(ctx, apiv1.ListNodesParams{User: apiv1.NewOptString("alice")})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, byUser.Nodes, 2)
|
||||
}
|
||||
|
||||
func TestAPIv1_DeleteNode(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
user := srv.CreateUser(t, "alice")
|
||||
node := srv.CreateNode(t, user, "node1")
|
||||
|
||||
require.NoError(t, client.DeleteNode(ctx, apiv1.DeleteNodeParams{NodeID: uint64(node.ID)}))
|
||||
|
||||
_, err := client.GetNode(ctx, apiv1.GetNodeParams{NodeID: uint64(node.ID)})
|
||||
requireProblem(t, err, http.StatusNotFound)
|
||||
|
||||
err = client.DeleteNode(ctx, apiv1.DeleteNodeParams{NodeID: 99999})
|
||||
requireProblem(t, err, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIv1_RenameNode(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
user := srv.CreateUser(t, "alice")
|
||||
node := srv.CreateNode(t, user, "node1")
|
||||
|
||||
resp, err := client.RenameNode(ctx, apiv1.RenameNodeParams{
|
||||
NodeID: uint64(node.ID),
|
||||
NewName: "renamed",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "renamed", resp.Node.Value.GivenName.Value)
|
||||
}
|
||||
|
||||
func TestAPIv1_ExpireNode(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
user := srv.CreateUser(t, "alice")
|
||||
node := srv.CreateNode(t, user, "node1")
|
||||
|
||||
resp, err := client.ExpireNode(ctx, apiv1.ExpireNodeParams{NodeID: uint64(node.ID)})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, resp.Node.Value.Expiry.Set, "expiry should be set")
|
||||
|
||||
// Disabling expiry clears it.
|
||||
resp, err = client.ExpireNode(ctx, apiv1.ExpireNodeParams{
|
||||
NodeID: uint64(node.ID),
|
||||
DisableExpiry: apiv1.NewOptBool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.False(t, resp.Node.Value.Expiry.Set, "expiry should be cleared")
|
||||
|
||||
// Setting both expiry and disable_expiry is a 400.
|
||||
_, err = client.ExpireNode(ctx, apiv1.ExpireNodeParams{
|
||||
NodeID: uint64(node.ID),
|
||||
Expiry: apiv1.NewOptDateTime(time.Now()),
|
||||
DisableExpiry: apiv1.NewOptBool(true),
|
||||
})
|
||||
requireProblem(t, err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestAPIv1_SetApprovedRoutes_ExitNodeExpansion(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
user := srv.CreateUser(t, "alice")
|
||||
node := srv.CreateNode(t, user, "node1")
|
||||
|
||||
resp, err := client.SetApprovedRoutes(
|
||||
ctx,
|
||||
&apiv1.SetApprovedRoutesReq{Routes: []string{"0.0.0.0/0"}},
|
||||
apiv1.SetApprovedRoutesParams{NodeID: uint64(node.ID)},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
// An exit route is expanded to both default routes.
|
||||
assert.Contains(t, resp.Node.Value.ApprovedRoutes, "0.0.0.0/0")
|
||||
assert.Contains(t, resp.Node.Value.ApprovedRoutes, "::/0")
|
||||
}
|
||||
|
||||
func TestAPIv1_SetTags_Validation(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
user := srv.CreateUser(t, "alice")
|
||||
node := srv.CreateNode(t, user, "node1")
|
||||
|
||||
// Empty tag list is a 400.
|
||||
_, err := client.SetTags(
|
||||
ctx,
|
||||
&apiv1.SetTagsReq{Tags: []string{}},
|
||||
apiv1.SetTagsParams{NodeID: uint64(node.ID)},
|
||||
)
|
||||
requireProblem(t, err, http.StatusBadRequest)
|
||||
|
||||
// Malformed tag is a 400.
|
||||
_, err = client.SetTags(
|
||||
ctx,
|
||||
&apiv1.SetTagsReq{Tags: []string{"notatag"}},
|
||||
apiv1.SetTagsParams{NodeID: uint64(node.ID)},
|
||||
)
|
||||
requireProblem(t, err, http.StatusBadRequest)
|
||||
|
||||
// Unknown node is a 404.
|
||||
_, err = client.SetTags(
|
||||
ctx,
|
||||
&apiv1.SetTagsReq{Tags: []string{"tag:test"}},
|
||||
apiv1.SetTagsParams{NodeID: 99999},
|
||||
)
|
||||
requireProblem(t, err, http.StatusNotFound)
|
||||
|
||||
// Unauthorized tag (no tagOwners policy) is a 400.
|
||||
_, err = client.SetTags(
|
||||
ctx,
|
||||
&apiv1.SetTagsReq{Tags: []string{"tag:test"}},
|
||||
apiv1.SetTagsParams{NodeID: uint64(node.ID)},
|
||||
)
|
||||
requireProblem(t, err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestAPIv1_BackfillNodeIPs(t *testing.T) {
|
||||
_, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Without confirmation it is a 400.
|
||||
_, err := client.BackfillNodeIPs(ctx, apiv1.BackfillNodeIPsParams{})
|
||||
requireProblem(t, err, http.StatusBadRequest)
|
||||
|
||||
// Confirmed succeeds.
|
||||
_, err = client.BackfillNodeIPs(ctx, apiv1.BackfillNodeIPsParams{
|
||||
Confirmed: apiv1.NewOptBool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAPIv1_RegisterNode_Errors(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
srv.CreateUser(t, "alice")
|
||||
|
||||
// Malformed registration key is a 400.
|
||||
_, err := client.RegisterNode(ctx, apiv1.RegisterNodeParams{
|
||||
User: apiv1.NewOptString("alice"),
|
||||
Key: apiv1.NewOptString("not-a-valid-key"),
|
||||
})
|
||||
requireProblem(t, err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestAPIv1_DebugCreateNode(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
srv.CreateUser(t, "alice")
|
||||
|
||||
resp, err := client.DebugCreateNode(ctx, &apiv1.DebugCreateNodeReq{
|
||||
User: apiv1.NewOptString("alice"),
|
||||
Key: apiv1.NewOptString(types.MustAuthID().String()),
|
||||
Name: apiv1.NewOptString("debug-node"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "debug-node", resp.Node.Value.Name.Value)
|
||||
assert.Equal(t, "alice", resp.Node.Value.User.Value.Name.Value)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package servertest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const validPolicy = `{"acls":[{"action":"accept","src":["*"],"dst":["*:*"]}]}`
|
||||
|
||||
func TestAPIv1_SetAndGetPolicy(t *testing.T) {
|
||||
_, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
setResp, err := client.SetPolicy(ctx, &apiv1.SetPolicyReq{
|
||||
Policy: apiv1.NewOptString(validPolicy),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, setResp.Policy.Value)
|
||||
|
||||
getResp, err := client.GetPolicy(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, setResp.Policy.Value, getResp.Policy.Value)
|
||||
}
|
||||
|
||||
func TestAPIv1_CheckPolicy(t *testing.T) {
|
||||
_, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, client.CheckPolicy(ctx, &apiv1.CheckPolicyReq{
|
||||
Policy: apiv1.NewOptString(validPolicy),
|
||||
}))
|
||||
|
||||
// Invalid policy is a 400.
|
||||
requireProblem(t, client.CheckPolicy(ctx, &apiv1.CheckPolicyReq{
|
||||
Policy: apiv1.NewOptString("{not valid"),
|
||||
}), http.StatusBadRequest)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package servertest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIv1_CreatePreAuthKey(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
u := srv.CreateUser(t, "alice")
|
||||
|
||||
resp, err := client.CreatePreAuthKey(ctx, &apiv1.CreatePreAuthKeyReq{
|
||||
User: apiv1.NewOptUint64(uint64(u.ID)),
|
||||
Reusable: apiv1.NewOptBool(true),
|
||||
AclTags: []string{"tag:test"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
pak := resp.PreAuthKey.Value
|
||||
assert.NotEmpty(t, pak.Key.Value)
|
||||
assert.True(t, pak.Reusable.Value)
|
||||
assert.Equal(t, []string{"tag:test"}, pak.AclTags)
|
||||
assert.Equal(t, uint64(u.ID), pak.User.Value.ID.Value)
|
||||
|
||||
// Invalid tag format is a 400.
|
||||
_, err = client.CreatePreAuthKey(ctx, &apiv1.CreatePreAuthKeyReq{
|
||||
User: apiv1.NewOptUint64(uint64(u.ID)),
|
||||
AclTags: []string{"badtag"},
|
||||
})
|
||||
requireProblem(t, err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestAPIv1_ListPreAuthKeys(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
u := srv.CreateUser(t, "alice")
|
||||
|
||||
srv.CreatePreAuthKey(t, types.UserID(u.ID))
|
||||
srv.CreatePreAuthKey(t, types.UserID(u.ID))
|
||||
|
||||
resp, err := client.ListPreAuthKeys(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.PreAuthKeys, 2)
|
||||
assert.LessOrEqual(t, resp.PreAuthKeys[0].ID.Value, resp.PreAuthKeys[1].ID.Value)
|
||||
}
|
||||
|
||||
func TestAPIv1_ExpirePreAuthKey(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
u := srv.CreateUser(t, "alice")
|
||||
|
||||
resp, err := client.CreatePreAuthKey(ctx, &apiv1.CreatePreAuthKeyReq{
|
||||
User: apiv1.NewOptUint64(uint64(u.ID)),
|
||||
Reusable: apiv1.NewOptBool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
id := resp.PreAuthKey.Value.ID.Value
|
||||
|
||||
require.NoError(t, client.ExpirePreAuthKey(ctx, &apiv1.ExpirePreAuthKeyReq{
|
||||
ID: apiv1.NewOptUint64(id),
|
||||
}))
|
||||
|
||||
keys, err := srv.State().ListPreAuthKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
found := false
|
||||
|
||||
for _, k := range keys {
|
||||
if k.ID == id {
|
||||
found = true
|
||||
|
||||
require.NotNil(t, k.Expiration)
|
||||
assert.True(t, k.Expiration.Before(time.Now()), "key should be expired")
|
||||
}
|
||||
}
|
||||
|
||||
require.True(t, found, "expired key should still be listed")
|
||||
}
|
||||
|
||||
func TestAPIv1_DeletePreAuthKey(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
u := srv.CreateUser(t, "alice")
|
||||
|
||||
resp, err := client.CreatePreAuthKey(ctx, &apiv1.CreatePreAuthKeyReq{
|
||||
User: apiv1.NewOptUint64(uint64(u.ID)),
|
||||
Reusable: apiv1.NewOptBool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
id := resp.PreAuthKey.Value.ID.Value
|
||||
|
||||
require.NoError(t, client.DeletePreAuthKey(ctx, apiv1.DeletePreAuthKeyParams{
|
||||
ID: apiv1.NewOptUint64(id),
|
||||
}))
|
||||
|
||||
keys, err := srv.State().ListPreAuthKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, k := range keys {
|
||||
assert.NotEqual(t, id, k.ID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package servertest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
apiv1 "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIv1_CreateUser(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
resp, err := client.CreateUser(ctx, &apiv1.CreateUserReq{
|
||||
Name: apiv1.NewOptString("alice"),
|
||||
DisplayName: apiv1.NewOptString("Alice"),
|
||||
Email: apiv1.NewOptString("alice@example.com"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
user := resp.User.Value
|
||||
assert.NotZero(t, user.ID.Value)
|
||||
assert.Equal(t, "alice", user.Name.Value)
|
||||
assert.Equal(t, "Alice", user.DisplayName.Value)
|
||||
assert.Equal(t, "alice@example.com", user.Email.Value)
|
||||
|
||||
// Side effect: the user is persisted.
|
||||
got, err := srv.State().GetUserByName("alice")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(got.ID), user.ID.Value)
|
||||
}
|
||||
|
||||
func TestAPIv1_ListUsers(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
srv.CreateUser(t, "alice")
|
||||
srv.CreateUser(t, "bob")
|
||||
|
||||
all, err := client.ListUsers(ctx, apiv1.ListUsersParams{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, all.Users, 2)
|
||||
// Sorted by id ascending.
|
||||
assert.Less(t, all.Users[0].ID.Value, all.Users[1].ID.Value)
|
||||
|
||||
byName, err := client.ListUsers(ctx, apiv1.ListUsersParams{Name: apiv1.NewOptString("bob")})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, byName.Users, 1)
|
||||
assert.Equal(t, "bob", byName.Users[0].Name.Value)
|
||||
|
||||
none, err := client.ListUsers(ctx, apiv1.ListUsersParams{Name: apiv1.NewOptString("nobody")})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, none.Users)
|
||||
}
|
||||
|
||||
func TestAPIv1_RenameUser(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
u := srv.CreateUser(t, "alice")
|
||||
|
||||
resp, err := client.RenameUser(ctx, apiv1.RenameUserParams{
|
||||
OldID: uint64(u.ID),
|
||||
NewName: "alice2",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "alice2", resp.User.Value.Name.Value)
|
||||
|
||||
_, err = srv.State().GetUserByName("alice2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Unknown user is a 404 (the previous implementation returned 500 here).
|
||||
_, err = client.RenameUser(ctx, apiv1.RenameUserParams{OldID: 99999, NewName: "ghost"})
|
||||
requireProblem(t, err, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIv1_DeleteUser(t *testing.T) {
|
||||
srv, client := apiClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
u := srv.CreateUser(t, "alice")
|
||||
|
||||
require.NoError(t, client.DeleteUser(ctx, apiv1.DeleteUserParams{ID: uint64(u.ID)}))
|
||||
|
||||
_, err := srv.State().GetUserByName("alice")
|
||||
require.Error(t, err)
|
||||
|
||||
// Deleting an unknown user is a 404.
|
||||
err = client.DeleteUser(ctx, apiv1.DeleteUserParams{ID: 99999})
|
||||
requireProblem(t, err, http.StatusNotFound)
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func persistTestConfig(dbPath string) *types.Config {
|
||||
}
|
||||
|
||||
// TestPersistEmptyApprovedRoutes covers the State.SetApprovedRoutes
|
||||
// path. The gRPC handler builds the slice via append from a nil
|
||||
// path. The handler builds the slice via append from a nil
|
||||
// declaration, so when the operator passes `-r ""` the persist layer
|
||||
// receives a nil []netip.Prefix. GORM's struct Updates skips nil
|
||||
// slices, so the column would stay populated with the previously
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user