You've already forked ionscale
mirror of
https://github.com/jsiebens/ionscale.git
synced 2026-04-05 12:32:58 +01:00
Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45572397ea | |||
| e5a3d3c589 | |||
| 2a5fe7f136 | |||
| 7ee4b27688 | |||
| 69f7c22307 | |||
| 4e5f89ab7e | |||
| c1ffe03e81 | |||
| 7ad91c4c20 | |||
| fb04248db4 | |||
| d84bad12d0 | |||
| cadf938e2a | |||
| 980ae6dd85 | |||
| 6e3e22bc72 | |||
| 0051eec355 | |||
| 617575803c | |||
| 8c6ea9041b | |||
| c6ebeb36bc | |||
| d87c7252c2 | |||
| bfcf0c7925 | |||
| aea3d2d6a9 | |||
| 9781e75833 | |||
| 47b15d31f0 | |||
| ec353f7add | |||
| 92ca75b7f4 | |||
| 1702cf135e | |||
| b65119bbba | |||
| b265fc42c7 | |||
| 69dd1f6b95 | |||
| ebf0016096 | |||
| 3aa2d68ce2 | |||
| 3d03f49138 | |||
| c4783f8165 | |||
| 3b9ce04ec8 | |||
| f71ca49693 | |||
| 61d78fe121 | |||
| 5b51e29140 | |||
| e5ed4713d8 | |||
| 9281deb549 | |||
| 88509c826d | |||
| 405110867a | |||
| 82c814aa2a | |||
| 5a524d7357 | |||
| 0f0829ccba | |||
| 4c9ea463db | |||
| 284ec18339 | |||
| 5a77d2b35b | |||
| c193a4bf71 | |||
| 550febc5ba | |||
| f0d71c8a66 | |||
| 3c50d4869d | |||
| e8fe0e2467 | |||
| 633f29003c | |||
| 145ae6ab1d | |||
| b60e332cbd | |||
| f38939415d | |||
| 49e5c7999f | |||
| 82a28e32c0 | |||
| 7976e7aa83 | |||
| 404b667aaf | |||
| 6700d0db01 | |||
| 25ee5a21a6 | |||
| d735974406 | |||
| 41827dcdcd | |||
| cd1854f510 | |||
| 6a6049b76b | |||
| 50d52ae481 | |||
| 402f98b688 | |||
| 4234c5eed9 | |||
| 3568764ec1 | |||
| df02644437 | |||
| 7db10b563d | |||
| 496fd5f47c | |||
| 200b523ae0 | |||
| f225f427ac | |||
| 70e84be8f4 | |||
| 409dd3aa5f | |||
| 0d5ffa9c8b | |||
| 0756de5bfb | |||
| 32cb12e286 | |||
| f6961cf2f7 | |||
| ba379e1b65 | |||
| 12eb258e1e | |||
| 32c396a972 | |||
| d0e69cc2bf | |||
| 5e132392b3 | |||
| 58e1f38231 | |||
| d5f71224f6 | |||
| 090e5c3c88 | |||
| 8e8646b757 | |||
| a94e0ce9b8 | |||
| eefa150738 | |||
| bbe9d16294 | |||
| 5fdde45fdd | |||
| 1715eb681d | |||
| 9d29644941 | |||
| da71a43990 | |||
| 687fcd16d1 | |||
| b9b42d8342 | |||
| 1654680cab | |||
| 9df514036e |
@@ -0,0 +1,9 @@
|
||||
## Contribution Policy
|
||||
|
||||
ionscale is open to code contributions for bug fixes only.
|
||||
|
||||
Features carry a long-term maintenance burden so they will not be accepted at this time.
|
||||
Please [submit an issue][new-issue] if you have a feature you'd like to
|
||||
request.
|
||||
|
||||
[new-issue]: https://github.com/jsiebens/ionscale/issues/new
|
||||
@@ -4,21 +4,23 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.18
|
||||
go-version: 1.19
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
go test ./...
|
||||
go build cmd/ionscale/main.go
|
||||
@@ -0,0 +1,42 @@
|
||||
name: nightly
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: "enabled"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Docker Login
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@v2.5.1
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --nightly --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
DOCKER_CLI_EXPERIMENTAL: "enabled"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up QEMU
|
||||
@@ -31,9 +31,9 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.18
|
||||
go-version: 1.19
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@v2.0.0
|
||||
uses: sigstore/cosign-installer@v2.5.1
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
name: "Security Analysis"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
name: CodeQL
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: go
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
trivy:
|
||||
name: Trivy
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: ghcr.io/jsiebens/ionscale:latest
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
- name: Upload Trivy scan results
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
+7
-2
@@ -1,3 +1,8 @@
|
||||
FROM alpine:3.15.4
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} alpine:3.16.2
|
||||
|
||||
COPY ionscale /usr/local/bin/ionscale
|
||||
ENTRYPOINT ["ionscale"]
|
||||
|
||||
RUN mkdir -p /data/ionscale
|
||||
WORKDIR /data/ionscale
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/ionscale"]
|
||||
@@ -1,2 +1,8 @@
|
||||
init:
|
||||
go install github.com/bufbuild/buf/cmd/buf@latest
|
||||
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
go install github.com/bufbuild/connect-go/cmd/protoc-gen-connect-go@latest
|
||||
|
||||
generate:
|
||||
buf generate proto
|
||||
@@ -1 +1,4 @@
|
||||
# ionscale
|
||||
|
||||
> **Note**:
|
||||
> ionscale is currently alpha quality, actively being developed and so subject to changes
|
||||
+3
-6
@@ -1,11 +1,8 @@
|
||||
version: v1beta1
|
||||
version: v1
|
||||
plugins:
|
||||
- name: go
|
||||
out: pkg/gen
|
||||
opt: paths=source_relative
|
||||
- name: go-grpc
|
||||
- name: connect-go
|
||||
out: pkg/gen
|
||||
opt: paths=source_relative,require_unimplemented_servers=false
|
||||
- name: grpc-gateway
|
||||
out: pkg/gen
|
||||
opt: paths=source_relative
|
||||
opt: paths=source_relative
|
||||
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/muesli/coral"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd := rootCommand()
|
||||
if err := cmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func rootCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "pg-ionscale-events",
|
||||
}
|
||||
|
||||
var url string
|
||||
command.Flags().StringVar(&url, "url", "", "")
|
||||
_ = command.MarkFlagRequired("url")
|
||||
|
||||
command.RunE = func(cmd *coral.Command, args []string) error {
|
||||
_, err := sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reportProblem := func(ev pq.ListenerEventType, err error) {
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
minReconn := 10 * time.Second
|
||||
maxReconn := time.Minute
|
||||
listener := pq.NewListener(url, minReconn, maxReconn, reportProblem)
|
||||
err = listener.Listen("ionscale_events")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("listening for events ...")
|
||||
fmt.Println("")
|
||||
for {
|
||||
select {
|
||||
case n, _ := <-listener.Notify:
|
||||
fmt.Println(n.Extra)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -1,102 +1,111 @@
|
||||
module github.com/jsiebens/ionscale
|
||||
|
||||
go 1.18
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/apparentlymart/go-cidr v1.1.0
|
||||
github.com/caddyserver/certmagic v0.16.1
|
||||
github.com/coreos/go-oidc/v3 v3.2.0
|
||||
github.com/glebarez/sqlite v1.4.3
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0-rc.2
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
|
||||
github.com/bufbuild/connect-go v0.4.0
|
||||
github.com/caarlos0/env/v6 v6.10.1
|
||||
github.com/caddyserver/certmagic v0.17.1
|
||||
github.com/coreos/go-oidc/v3 v3.3.0
|
||||
github.com/glebarez/sqlite v1.4.6
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.0.2
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/hashicorp/go-bexpr v0.1.11
|
||||
github.com/hashicorp/go-hclog v1.1.0
|
||||
github.com/improbable-eng/grpc-web v0.15.0
|
||||
github.com/klauspost/compress v1.15.3
|
||||
github.com/labstack/echo-contrib v0.12.0
|
||||
github.com/labstack/echo/v4 v4.6.3
|
||||
github.com/hashicorp/go-hclog v1.3.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/imdario/mergo v0.3.12
|
||||
github.com/klauspost/compress v1.15.9
|
||||
github.com/labstack/echo-contrib v0.13.0
|
||||
github.com/labstack/echo/v4 v4.9.0
|
||||
github.com/lib/pq v1.10.6
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/pointerstructure v1.2.1
|
||||
github.com/mr-tron/base58 v1.2.0
|
||||
github.com/muesli/coral v1.0.0
|
||||
github.com/nleeper/goment v1.4.4
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/rodaine/table v1.0.1
|
||||
github.com/soheilhy/cmux v0.1.5
|
||||
github.com/sony/sonyflake v1.0.0
|
||||
github.com/sony/sonyflake v1.1.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/xhit/go-str2duration/v2 v2.0.0
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
google.golang.org/grpc v1.44.0
|
||||
google.golang.org/protobuf v1.27.1
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
|
||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b
|
||||
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
|
||||
google.golang.org/protobuf v1.28.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.0
|
||||
gorm.io/gorm v1.23.5
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
||||
tailscale.com v1.24.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/postgres v1.3.9
|
||||
gorm.io/gorm v1.23.8
|
||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317
|
||||
tailscale.com v1.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.16.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.18.1 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.7 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/google/go-cmp v0.5.8 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.13.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgtype v1.12.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.17.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/native v1.0.0 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.11 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.2.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.1.1 // indirect
|
||||
github.com/labstack/gommon v0.3.1 // indirect
|
||||
github.com/libdns/libdns v0.2.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mdlayher/netlink v1.6.0 // indirect
|
||||
github.com/mdlayher/socket v0.2.3 // indirect
|
||||
github.com/mholt/acmez v1.0.2 // indirect
|
||||
github.com/miekg/dns v1.1.46 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/mitchellh/pointerstructure v1.2.1 // indirect
|
||||
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||
github.com/mholt/acmez v1.0.4 // indirect
|
||||
github.com/miekg/dns v1.1.50 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/rs/cors v1.7.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tkuchiki/go-timezone v0.2.0 // indirect
|
||||
github.com/tkuchiki/go-timezone v0.2.2 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.21.0 // indirect
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||
golang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
go.uber.org/zap v1.23.0 // indirect
|
||||
go4.org/intern v0.0.0-20220617035311-6925f38cc365 // indirect
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
|
||||
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 // indirect
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10 // indirect
|
||||
google.golang.org/appengine v1.6.6 // indirect
|
||||
google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
modernc.org/libc v1.14.12 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.0.7 // indirect
|
||||
modernc.org/sqlite v1.16.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
modernc.org/libc v1.18.0 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.3.0 // indirect
|
||||
modernc.org/sqlite v1.18.1 // indirect
|
||||
nhooyr.io/websocket v1.8.7 // indirect
|
||||
)
|
||||
|
||||
+28
-12
@@ -3,18 +3,34 @@ package addr
|
||||
import (
|
||||
"github.com/apparentlymart/go-cidr/cidr"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"inet.af/netaddr"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/netip"
|
||||
"tailscale.com/net/tsaddr"
|
||||
)
|
||||
|
||||
var ipv4Range = tsaddr.CGNATRange().IPNet()
|
||||
var (
|
||||
ipv4Range *net.IPNet
|
||||
ipv4Count uint64
|
||||
)
|
||||
|
||||
type Predicate func(netaddr.IP) (bool, error)
|
||||
func init() {
|
||||
ipv4Range, ipv4Count = prepareIP4Range()
|
||||
}
|
||||
|
||||
func SelectIP(predicate Predicate) (*netaddr.IP, *netaddr.IP, error) {
|
||||
ip4, err := selectIP(ipv4Range, predicate)
|
||||
func prepareIP4Range() (*net.IPNet, uint64) {
|
||||
cgnatRange := tsaddr.CGNATRange()
|
||||
_, ipNet, err := net.ParseCIDR(cgnatRange.String())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ipNet, cidr.AddressCount(ipNet)
|
||||
}
|
||||
|
||||
type Predicate func(netip.Addr) (bool, error)
|
||||
|
||||
func SelectIP(predicate Predicate) (*netip.Addr, *netip.Addr, error) {
|
||||
ip4, err := selectIP(predicate)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -22,16 +38,16 @@ func SelectIP(predicate Predicate) (*netaddr.IP, *netaddr.IP, error) {
|
||||
return ip4, &ip6, err
|
||||
}
|
||||
|
||||
func selectIP(c *net.IPNet, predicate Predicate) (*netaddr.IP, error) {
|
||||
count := cidr.AddressCount(c)
|
||||
var n = util.RandUint64(count)
|
||||
func selectIP(predicate Predicate) (*netip.Addr, error) {
|
||||
var n = util.RandUint64(ipv4Count)
|
||||
|
||||
for {
|
||||
stdIP, err := cidr.HostBig(c, big.NewInt(int64(n)))
|
||||
stdIP, err := cidr.HostBig(ipv4Range, big.NewInt(int64(n)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ip, _ := netaddr.FromStdIP(stdIP)
|
||||
|
||||
ip, _ := netip.AddrFromSlice(stdIP)
|
||||
ok, err := validateIP(ip, predicate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -39,11 +55,11 @@ func selectIP(c *net.IPNet, predicate Predicate) (*netaddr.IP, error) {
|
||||
if ok {
|
||||
return &ip, nil
|
||||
}
|
||||
n = (n + 1) % count
|
||||
n = (n + 1) % ipv4Count
|
||||
}
|
||||
}
|
||||
|
||||
func validateIP(ip netaddr.IP, p Predicate) (bool, error) {
|
||||
func validateIP(ip netip.Addr, p Predicate) (bool, error) {
|
||||
if tsaddr.IsTailscaleIP(ip) {
|
||||
if p != nil {
|
||||
return p(ip)
|
||||
|
||||
@@ -1,134 +1 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
type BrokerPool struct {
|
||||
lock sync.Mutex
|
||||
store map[uint64]Broker
|
||||
}
|
||||
|
||||
type Signal struct {
|
||||
PeerUpdated *uint64
|
||||
PeersRemoved []uint64
|
||||
ACLUpdated bool
|
||||
DNSUpdated bool
|
||||
derpMap *tailcfg.DERPMap
|
||||
}
|
||||
|
||||
type Broker interface {
|
||||
AddClient(*Client)
|
||||
RemoveClient(uint64)
|
||||
|
||||
SignalUpdate()
|
||||
SignalPeerUpdated(id uint64)
|
||||
SignalPeersRemoved([]uint64)
|
||||
SignalDNSUpdated()
|
||||
SignalACLUpdated()
|
||||
SignalDERPMapUpdated(c *tailcfg.DERPMap)
|
||||
|
||||
IsConnected(uint64) bool
|
||||
}
|
||||
|
||||
func NewBrokerPool() *BrokerPool {
|
||||
return &BrokerPool{
|
||||
store: make(map[uint64]Broker),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BrokerPool) Get(tailnetID uint64) Broker {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
b, ok := m.store[tailnetID]
|
||||
if !ok {
|
||||
b = newBroker(tailnetID)
|
||||
m.store[tailnetID] = b
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (m *BrokerPool) SignalDERPMapUpdated(c *tailcfg.DERPMap) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
for _, b := range m.store {
|
||||
b.SignalDERPMapUpdated(c)
|
||||
}
|
||||
}
|
||||
|
||||
func newBroker(tailnetID uint64) Broker {
|
||||
b := &broker{
|
||||
tailnetID: tailnetID,
|
||||
newClients: make(chan *Client),
|
||||
closingClients: make(chan uint64),
|
||||
clients: make(map[uint64]*Client),
|
||||
signalChannel: make(chan *Signal),
|
||||
}
|
||||
|
||||
go b.listen()
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
type broker struct {
|
||||
tailnetID uint64
|
||||
privateKey *key.MachinePrivate
|
||||
newClients chan *Client
|
||||
closingClients chan uint64
|
||||
signalChannel chan *Signal
|
||||
clients map[uint64]*Client
|
||||
}
|
||||
|
||||
func (h *broker) IsConnected(id uint64) (ok bool) {
|
||||
_, ok = h.clients[id]
|
||||
return
|
||||
}
|
||||
|
||||
func (h *broker) AddClient(client *Client) {
|
||||
h.newClients <- client
|
||||
}
|
||||
|
||||
func (h *broker) RemoveClient(id uint64) {
|
||||
h.closingClients <- id
|
||||
}
|
||||
|
||||
func (h *broker) SignalUpdate() {
|
||||
h.signalChannel <- &Signal{}
|
||||
}
|
||||
|
||||
func (h *broker) SignalPeerUpdated(id uint64) {
|
||||
h.signalChannel <- &Signal{PeerUpdated: &id}
|
||||
}
|
||||
|
||||
func (h *broker) SignalPeersRemoved(ids []uint64) {
|
||||
h.signalChannel <- &Signal{PeersRemoved: ids}
|
||||
}
|
||||
|
||||
func (h *broker) SignalDNSUpdated() {
|
||||
h.signalChannel <- &Signal{DNSUpdated: true}
|
||||
}
|
||||
|
||||
func (h *broker) SignalACLUpdated() {
|
||||
h.signalChannel <- &Signal{ACLUpdated: true}
|
||||
}
|
||||
|
||||
func (h *broker) SignalDERPMapUpdated(c *tailcfg.DERPMap) {
|
||||
h.signalChannel <- &Signal{derpMap: c}
|
||||
}
|
||||
|
||||
func (h *broker) listen() {
|
||||
for {
|
||||
select {
|
||||
case s := <-h.newClients:
|
||||
h.clients[s.id] = s
|
||||
case s := <-h.closingClients:
|
||||
delete(h.clients, s)
|
||||
case s := <-h.signalChannel:
|
||||
for _, c := range h.clients {
|
||||
c.SignalUpdate(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"github.com/jsiebens/ionscale/internal/bind"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func NewClient(id uint64, channel chan *Signal) Client {
|
||||
return Client{
|
||||
id: id,
|
||||
channel: channel,
|
||||
}
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
id uint64
|
||||
binder bind.Binder
|
||||
node *tailcfg.Node
|
||||
|
||||
compress string
|
||||
channel chan *Signal
|
||||
}
|
||||
|
||||
func (c *Client) SignalUpdate(s *Signal) {
|
||||
c.channel <- s
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package broker
|
||||
|
||||
type Signal struct {
|
||||
PeerUpdated *uint64
|
||||
PeersRemoved []uint64
|
||||
ACLUpdated bool
|
||||
DNSUpdated bool
|
||||
}
|
||||
|
||||
type Listener chan *Signal
|
||||
|
||||
type Pubsub interface {
|
||||
Subscribe(tailnet uint64, listener Listener) (cancel func(), err error)
|
||||
Publish(tailnet uint64, message *Signal) error
|
||||
Close() error
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type memoryPubsub struct {
|
||||
mut sync.RWMutex
|
||||
listeners map[uint64]map[uuid.UUID]Listener
|
||||
}
|
||||
|
||||
func (m *memoryPubsub) Subscribe(tailnet uint64, listener Listener) (cancel func(), err error) {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
var listeners map[uuid.UUID]Listener
|
||||
var ok bool
|
||||
if listeners, ok = m.listeners[tailnet]; !ok {
|
||||
listeners = map[uuid.UUID]Listener{}
|
||||
m.listeners[tailnet] = listeners
|
||||
}
|
||||
var id uuid.UUID
|
||||
for {
|
||||
id = uuid.New()
|
||||
if _, ok = listeners[id]; !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
listeners[id] = listener
|
||||
return func() {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
listeners := m.listeners[tailnet]
|
||||
delete(listeners, id)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *memoryPubsub) Publish(tailnet uint64, message *Signal) error {
|
||||
m.mut.RLock()
|
||||
defer m.mut.RUnlock()
|
||||
listeners, ok := m.listeners[tailnet]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, listener := range listeners {
|
||||
listener <- message
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*memoryPubsub) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewPubsubInMemory() Pubsub {
|
||||
return &memoryPubsub{
|
||||
listeners: make(map[uint64]map[uuid.UUID]Listener),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/lib/pq"
|
||||
"time"
|
||||
)
|
||||
|
||||
type pgPubsub struct {
|
||||
pgListener *pq.Listener
|
||||
db *sql.DB
|
||||
target Pubsub
|
||||
}
|
||||
|
||||
func NewPubsub(ctx context.Context, database *sql.DB, connectURL string) (Pubsub, error) {
|
||||
errCh := make(chan error)
|
||||
listener := pq.NewListener(connectURL, time.Second, time.Minute, func(event pq.ListenerEventType, err error) {
|
||||
select {
|
||||
case <-errCh:
|
||||
return
|
||||
default:
|
||||
errCh <- err
|
||||
close(errCh)
|
||||
}
|
||||
})
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create pq listener: %w", err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
if err := listener.Listen("ionscale_events"); err != nil {
|
||||
return nil, fmt.Errorf("listen: %w", err)
|
||||
}
|
||||
|
||||
pubsub := &pgPubsub{
|
||||
db: database,
|
||||
pgListener: listener,
|
||||
target: NewPubsubInMemory(),
|
||||
}
|
||||
go pubsub.listen(ctx)
|
||||
|
||||
return pubsub, nil
|
||||
}
|
||||
|
||||
func (p *pgPubsub) Close() error {
|
||||
return p.pgListener.Close()
|
||||
}
|
||||
|
||||
func (p *pgPubsub) Subscribe(tailnet uint64, listener Listener) (cancel func(), err error) {
|
||||
return p.target.Subscribe(tailnet, listener)
|
||||
}
|
||||
|
||||
func (p *pgPubsub) Publish(tailnet uint64, message *Signal) error {
|
||||
event := &pgEvent{
|
||||
TailnetID: tailnet,
|
||||
Signal: message,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = p.db.ExecContext(context.Background(), `select pg_notify(`+pq.QuoteLiteral("ionscale_events")+`, $1)`, payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("exec pg_notify: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *pgPubsub) listen(ctx context.Context) {
|
||||
var (
|
||||
notif *pq.Notification
|
||||
ok bool
|
||||
)
|
||||
defer p.pgListener.Close()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case notif, ok = <-p.pgListener.Notify:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
// A nil notification can be dispatched on reconnect.
|
||||
if notif == nil {
|
||||
continue
|
||||
}
|
||||
p.listenReceive(notif)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pgPubsub) listenReceive(notif *pq.Notification) {
|
||||
extra := []byte(notif.Extra)
|
||||
event := &pgEvent{}
|
||||
|
||||
if err := json.Unmarshal(extra, event); err == nil {
|
||||
p.target.Publish(event.TailnetID, event.Signal)
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
type pgEvent struct {
|
||||
TailnetID uint64
|
||||
Signal *Signal
|
||||
}
|
||||
+28
-42
@@ -4,16 +4,15 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"github.com/bufbuild/connect-go"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
func getACLConfig() *coral.Command {
|
||||
func getACLConfigCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "get-acl",
|
||||
Use: "get-acl-policy",
|
||||
Short: "Get the ACL policy",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
@@ -24,50 +23,33 @@ func getACLConfig() *coral.Command {
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "")
|
||||
command.Flags().BoolVar(&asJson, "json", false, "")
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
command.Flags().BoolVar(&asJson, "json", false, "When enabled, render output as json otherwise yaml")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, c, err := target.createGRPCClient()
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(cmd *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := client.GetACLPolicy(context.Background(), &api.GetACLPolicyRequest{TailnetId: tailnet.Id})
|
||||
resp, err := client.GetACLPolicy(context.Background(), connect.NewRequest(&api.GetACLPolicyRequest{TailnetId: tailnet.Id}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var p domain.ACLPolicy
|
||||
|
||||
if err := json.Unmarshal(resp.Value, &p); err != nil {
|
||||
marshal, err := json.MarshalIndent(resp.Msg.Policy, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if asJson {
|
||||
marshal, err := json.MarshalIndent(&p, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(string(marshal))
|
||||
} else {
|
||||
marshal, err := yaml.Marshal(&p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(string(marshal))
|
||||
}
|
||||
fmt.Println(string(marshal))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -75,9 +57,9 @@ func getACLConfig() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func setACLConfig() *coral.Command {
|
||||
func setACLConfigCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "set-acl",
|
||||
Use: "set-acl-policy",
|
||||
Short: "Set ACL policy",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
@@ -88,33 +70,37 @@ func setACLConfig() *coral.Command {
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "")
|
||||
command.Flags().StringVar(&file, "file", "", "")
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
command.Flags().StringVar(&file, "file", "", "Path to json file with the acl configuration")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(cmd *coral.Command, args []string) error {
|
||||
rawJson, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, c, err := target.createGRPCClient()
|
||||
var policy = &api.ACLPolicy{}
|
||||
if err := json.Unmarshal(rawJson, policy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.SetACLPolicy(context.Background(), &api.SetACLPolicyRequest{TailnetId: tailnet.Id, Value: rawJson})
|
||||
_, err = client.SetACLPolicy(context.Background(), connect.NewRequest(&api.SetACLPolicyRequest{TailnetId: tailnet.Id, Policy: policy}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("ACL policy updated successfully")
|
||||
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
)
|
||||
|
||||
func authCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "auth",
|
||||
}
|
||||
|
||||
command.AddCommand(authLoginCommand())
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func authLoginCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "login",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &api.AuthenticationRequest{}
|
||||
stream, err := client.Authenticate(context.Background(), connect.NewRequest(req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var started = false
|
||||
for stream.Receive() {
|
||||
resp := stream.Msg()
|
||||
if len(resp.Token) != 0 {
|
||||
fmt.Println()
|
||||
fmt.Println("Success.")
|
||||
if err := ionscale.SessionToFile(resp.Token, resp.TailnetId); err != nil {
|
||||
fmt.Println()
|
||||
fmt.Println("Your api token:")
|
||||
fmt.Println()
|
||||
fmt.Printf(" %s\n", resp.Token)
|
||||
fmt.Println()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(resp.AuthUrl) != 0 && !started {
|
||||
started = true
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("To authenticate, visit:")
|
||||
fmt.Println()
|
||||
fmt.Printf(" %s\n", resp.AuthUrl)
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
if err := stream.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/rodaine/table"
|
||||
)
|
||||
|
||||
func authFilterCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "auth-filters",
|
||||
Short: "Manage ionscale auth filters",
|
||||
Long: `This command allows operations on ionscale auth filter resources. Example:
|
||||
|
||||
$ ionscale auth-filter create`,
|
||||
}
|
||||
|
||||
command.AddCommand(createAuthFilterCommand())
|
||||
command.AddCommand(listAuthFilterCommand())
|
||||
command.AddCommand(deleteAuthFilterCommand())
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func listAuthFilterCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "list",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var authMethodID uint64
|
||||
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.Flags().Uint64Var(&authMethodID, "auth-method-id", 0, "")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, c, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
req := &api.ListAuthFiltersRequest{}
|
||||
|
||||
if authMethodID != 0 {
|
||||
req.AuthMethodId = &authMethodID
|
||||
}
|
||||
|
||||
resp, err := client.ListAuthFilters(context.Background(), req)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tbl := table.New("ID", "AUTH_METHOD", "TAILNET", "EXPR")
|
||||
for _, filter := range resp.AuthFilters {
|
||||
if filter.Tailnet != nil {
|
||||
tbl.AddRow(filter.Id, filter.AuthMethod.Name, filter.Tailnet.Name, filter.Expr)
|
||||
} else {
|
||||
tbl.AddRow(filter.Id, filter.AuthMethod.Name, "", filter.Expr)
|
||||
}
|
||||
}
|
||||
tbl.Print()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func createAuthFilterCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "create",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var expr string
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
var authMethodID uint64
|
||||
var authMethodName string
|
||||
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.Flags().StringVar(&expr, "expr", "*", "")
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "")
|
||||
command.Flags().StringVar(&authMethodName, "auth-method", "", "")
|
||||
command.Flags().Uint64Var(&authMethodID, "auth-method-id", 0, "")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
if expr != "*" {
|
||||
if _, err := bexpr.CreateEvaluator(expr); err != nil {
|
||||
return fmt.Errorf("invalid expression: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
client, c, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authMethod, err := findAuthMethod(client, authMethodName, authMethodID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &api.CreateAuthFilterRequest{
|
||||
AuthMethodId: authMethod.Id,
|
||||
TailnetId: tailnet.Id,
|
||||
Expr: expr,
|
||||
}
|
||||
|
||||
resp, err := client.CreateAuthFilter(context.Background(), req)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tbl := table.New("ID", "AUTH_METHOD", "TAILNET", "EXPR")
|
||||
if resp.AuthFilter.Tailnet != nil {
|
||||
tbl.AddRow(resp.AuthFilter.Id, resp.AuthFilter.AuthMethod.Name, resp.AuthFilter.Tailnet.Name, resp.AuthFilter.Expr)
|
||||
} else {
|
||||
tbl.AddRow(resp.AuthFilter.Id, resp.AuthFilter.AuthMethod.Name, "", resp.AuthFilter.Expr)
|
||||
}
|
||||
tbl.Print()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func deleteAuthFilterCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "delete",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var authFilterID uint64
|
||||
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.Flags().Uint64Var(&authFilterID, "auth-filter-id", 0, "")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, c, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
req := &api.DeleteAuthFilterRequest{
|
||||
AuthFilterId: authFilterID,
|
||||
}
|
||||
|
||||
_, err = client.DeleteAuthFilter(context.Background(), req)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
+25
-21
@@ -3,7 +3,8 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"github.com/bufbuild/connect-go"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/rodaine/table"
|
||||
str2dur "github.com/xhit/go-str2duration/v2"
|
||||
@@ -14,7 +15,8 @@ import (
|
||||
|
||||
func authkeysCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "auth-keys",
|
||||
Use: "auth-keys",
|
||||
Short: "Manage ionscale auth keys",
|
||||
}
|
||||
|
||||
command.AddCommand(createAuthkeysCommand())
|
||||
@@ -27,6 +29,7 @@ func authkeysCommand() *coral.Command {
|
||||
func createAuthkeysCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "create",
|
||||
Short: "Creates a new auth key in the specified tailnet",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
@@ -38,18 +41,18 @@ func createAuthkeysCommand() *coral.Command {
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "")
|
||||
command.Flags().BoolVar(&ephemeral, "ephemeral", false, "")
|
||||
command.Flags().StringSliceVar(&tags, "tag", []string{}, "")
|
||||
command.Flags().StringVar(&expiry, "expiry", "180d", "")
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
command.Flags().BoolVar(&ephemeral, "ephemeral", false, "When enabled, machines authenticated by this key will be automatically removed after going offline.")
|
||||
command.Flags().StringSliceVar(&tags, "tag", []string{}, "Machines authenticated by this key will be automatically tagged with these tags")
|
||||
command.Flags().StringVar(&expiry, "expiry", "180d", "Human-readable expiration of the key")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, c, err := target.createGRPCClient()
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
@@ -72,7 +75,7 @@ func createAuthkeysCommand() *coral.Command {
|
||||
Tags: tags,
|
||||
Expiry: expiryDur,
|
||||
}
|
||||
resp, err := client.CreateAuthKey(context.Background(), req)
|
||||
resp, err := client.CreateAuthKey(context.Background(), connect.NewRequest(req))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -82,7 +85,7 @@ func createAuthkeysCommand() *coral.Command {
|
||||
fmt.Println("Generated new auth key")
|
||||
fmt.Println("Be sure to copy your new key below. It won't be shown in full again.")
|
||||
fmt.Println("")
|
||||
fmt.Printf(" %s\n", resp.Value)
|
||||
fmt.Printf(" %s\n", resp.Msg.Value)
|
||||
fmt.Println("")
|
||||
|
||||
return nil
|
||||
@@ -94,23 +97,23 @@ func createAuthkeysCommand() *coral.Command {
|
||||
func deleteAuthKeyCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "delete",
|
||||
Short: "Delete a specified auth key",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var authKeyId uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&authKeyId, "id", 0, "")
|
||||
command.Flags().Uint64Var(&authKeyId, "id", 0, "Auth Key ID")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
grpcClient, c, err := target.createGRPCClient()
|
||||
grpcClient, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
req := api.DeleteAuthKeyRequest{AuthKeyId: authKeyId}
|
||||
if _, err := grpcClient.DeleteAuthKey(context.Background(), &req); err != nil {
|
||||
if _, err := grpcClient.DeleteAuthKey(context.Background(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -125,6 +128,7 @@ func deleteAuthKeyCommand() *coral.Command {
|
||||
func listAuthkeysCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "list",
|
||||
Short: "List all auth keys for a given tailnet",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
@@ -133,15 +137,15 @@ func listAuthkeysCommand() *coral.Command {
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "")
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, c, err := target.createGRPCClient()
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
@@ -149,13 +153,13 @@ func listAuthkeysCommand() *coral.Command {
|
||||
}
|
||||
|
||||
req := &api.ListAuthKeysRequest{TailnetId: tailnet.Id}
|
||||
resp, err := client.ListAuthKeys(context.Background(), req)
|
||||
resp, err := client.ListAuthKeys(context.Background(), connect.NewRequest(req))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printAuthKeyTable(resp.AuthKeys...)
|
||||
printAuthKeyTable(resp.Msg.AuthKeys...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/rodaine/table"
|
||||
)
|
||||
|
||||
func authMethodsCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "auth-methods",
|
||||
Short: "Manage ionscale auth methods",
|
||||
}
|
||||
|
||||
command.AddCommand(listAuthMethods())
|
||||
command.AddCommand(createAuthMethodCommand())
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func listAuthMethods() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "list",
|
||||
Short: "List auth methods",
|
||||
Long: `List auth methods in this ionscale instance. Example:
|
||||
|
||||
$ ionscale auth-methods list`,
|
||||
}
|
||||
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
|
||||
client, c, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
resp, err := client.ListAuthMethods(context.Background(), &api.ListAuthMethodsRequest{})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tbl := table.New("ID", "NAME", "TYPE")
|
||||
for _, m := range resp.AuthMethods {
|
||||
tbl.AddRow(m.Id, m.Name, m.Type)
|
||||
}
|
||||
tbl.Print()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func createAuthMethodCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new auth method",
|
||||
}
|
||||
|
||||
command.AddCommand(createOIDCAuthMethodCommand())
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func createOIDCAuthMethodCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "oidc",
|
||||
Short: "Create a new auth method",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var methodName string
|
||||
|
||||
var clientId string
|
||||
var clientSecret string
|
||||
var issuer string
|
||||
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVarP(&methodName, "name", "n", "", "")
|
||||
command.Flags().StringVar(&clientId, "client-id", "", "")
|
||||
command.Flags().StringVar(&clientSecret, "client-secret", "", "")
|
||||
command.Flags().StringVar(&issuer, "issuer", "", "")
|
||||
|
||||
_ = command.MarkFlagRequired("name")
|
||||
_ = command.MarkFlagRequired("client-id")
|
||||
_ = command.MarkFlagRequired("client-secret")
|
||||
_ = command.MarkFlagRequired("issuer")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
|
||||
client, c, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
req := &api.CreateAuthMethodRequest{
|
||||
Type: "oidc",
|
||||
Name: methodName,
|
||||
Issuer: issuer,
|
||||
ClientId: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
}
|
||||
|
||||
resp, err := client.CreateAuthMethod(context.Background(), req)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tbl := table.New("ID", "NAME", "TYPE")
|
||||
tbl.AddRow(resp.AuthMethod.Id, resp.AuthMethod.Name, resp.AuthMethod.Type)
|
||||
tbl.Print()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/key"
|
||||
"github.com/muesli/coral"
|
||||
"gopkg.in/yaml.v2"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func configureCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "configure",
|
||||
Short: "Generate a simple config file to get started.",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var domain string
|
||||
var acme bool
|
||||
var email string
|
||||
var dataDir string
|
||||
var certFile string
|
||||
var keyFile string
|
||||
|
||||
command.Flags().StringVar(&domain, "domain", "", "Public domain name of your ionscale instance.")
|
||||
command.Flags().StringVar(&dataDir, "data-dir", "/var/lib/ionscale", "")
|
||||
command.Flags().BoolVar(&acme, "acme", false, "Get automatic certificate from Letsencrypt.org using ACME.")
|
||||
command.Flags().StringVar(&email, "acme-email", "", "Email to receive updates from Letsencrypt.org.")
|
||||
command.Flags().StringVar(&certFile, "cert-file", "", "Path to a TLS certificate file.")
|
||||
command.Flags().StringVar(&keyFile, "key-file", "", "Path to a TLS key file.")
|
||||
|
||||
command.MarkFlagRequired("domain")
|
||||
|
||||
command.PreRunE = func(cmd *coral.Command, args []string) error {
|
||||
if domain == "" {
|
||||
return errors.New("required flag 'domain' is missing")
|
||||
}
|
||||
|
||||
if acme && email == "" {
|
||||
return errors.New("flag 'acme-email' is required when acme is enabled")
|
||||
}
|
||||
|
||||
if !acme && (certFile == "" || keyFile == "") {
|
||||
return errors.New("flags 'cert-file' and 'key-file' are required when acme is disabled")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
c := &config.Config{}
|
||||
|
||||
c.HttpListenAddr = "0.0.0.0:80"
|
||||
c.HttpsListenAddr = "0.0.0.0:443"
|
||||
c.MetricsListenAddr = "127.0.0.1:9090"
|
||||
c.ServerUrl = fmt.Sprintf("https://%s", domain)
|
||||
|
||||
c.Keys = config.Keys{
|
||||
ControlKey: key.NewServerKey().String(),
|
||||
LegacyControlKey: key.NewServerKey().String(),
|
||||
SystemAdminKey: key.NewServerKey().String(),
|
||||
}
|
||||
|
||||
c.Tls = config.Tls{}
|
||||
if acme {
|
||||
c.Tls.AcmeEnabled = true
|
||||
c.Tls.AcmeEmail = email
|
||||
c.Tls.AcmePath = filepath.Join(dataDir, "acme")
|
||||
} else {
|
||||
c.Tls.CertFile = certFile
|
||||
c.Tls.KeyFile = keyFile
|
||||
}
|
||||
|
||||
c.Database = config.Database{
|
||||
Type: "sqlite",
|
||||
Url: filepath.Join(dataDir, "ionscale.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)"),
|
||||
}
|
||||
|
||||
configAsYaml, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(configAsYaml))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
+10
-11
@@ -4,7 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"github.com/bufbuild/connect-go"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
@@ -34,16 +35,15 @@ func getDERPMap() *coral.Command {
|
||||
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().BoolVar(&asJson, "json", false, "")
|
||||
command.Flags().BoolVar(&asJson, "json", false, "When enabled, render output as json otherwise yaml")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, c, err := target.createGRPCClient()
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
resp, err := client.GetDERPMap(context.Background(), &api.GetDERPMapRequest{})
|
||||
resp, err := client.GetDERPMap(context.Background(), connect.NewRequest(&api.GetDERPMapRequest{}))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -53,7 +53,7 @@ func getDERPMap() *coral.Command {
|
||||
Regions map[int]*tailcfg.DERPRegion
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(resp.Value, &derpMap); err != nil {
|
||||
if err := json.Unmarshal(resp.Msg.Value, &derpMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -91,27 +91,26 @@ func setDERPMap() *coral.Command {
|
||||
var file string
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&file, "file", "", "")
|
||||
command.Flags().StringVar(&file, "file", "", "Path to json file with the DERP Map configuration")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
grpcClient, c, err := target.createGRPCClient()
|
||||
grpcClient, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
rawJson, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := grpcClient.SetDERPMap(context.Background(), &api.SetDERPMapRequest{Value: rawJson})
|
||||
resp, err := grpcClient.SetDERPMap(context.Background(), connect.NewRequest(&api.SetDERPMapRequest{Value: rawJson}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var derpMap tailcfg.DERPMap
|
||||
if err := json.Unmarshal(resp.Value, &derpMap); err != nil {
|
||||
if err := json.Unmarshal(resp.Msg.Value, &derpMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+49
-22
@@ -3,12 +3,15 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"github.com/bufbuild/connect-go"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
func getDNSConfig() *coral.Command {
|
||||
func getDNSConfigCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "get-dns",
|
||||
Short: "Get DNS configuration",
|
||||
@@ -20,15 +23,15 @@ func getDNSConfig() *coral.Command {
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "")
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, c, err := target.createGRPCClient()
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
@@ -36,12 +39,12 @@ func getDNSConfig() *coral.Command {
|
||||
}
|
||||
|
||||
req := api.GetDNSConfigRequest{TailnetId: tailnet.Id}
|
||||
resp, err := client.GetDNSConfig(context.Background(), &req)
|
||||
resp, err := client.GetDNSConfig(context.Background(), connect.NewRequest(&req))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := resp.Config
|
||||
config := resp.Msg.Config
|
||||
|
||||
var allNameservers = config.Nameservers
|
||||
|
||||
@@ -51,9 +54,33 @@ func getDNSConfig() *coral.Command {
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("%-*v%v\n", 25, "Magic DNS Enabled:", config.MagicDns)
|
||||
fmt.Printf("%-*v%v\n", 25, "Override Local DNS:", config.OverrideLocalDns)
|
||||
fmt.Printf("%-*v%v\n", 25, "Nameservers:", strings.Join(allNameservers, ","))
|
||||
w := new(tabwriter.Writer)
|
||||
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintf(w, "%s\t\t%v\n", "Override Local DNS", config.OverrideLocalDns)
|
||||
|
||||
if config.MagicDns {
|
||||
fmt.Fprintf(w, "MagicDNS\t%s\t%s\n", config.MagicDnsSuffix, "100.100.100.100")
|
||||
}
|
||||
|
||||
for k, r := range config.Routes {
|
||||
for i, t := range r.Routes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "SplitDNS\t%s\t%s\n", k, t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, t := range config.Nameservers {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", "Global", "", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", "", "", t)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -61,7 +88,7 @@ func getDNSConfig() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func setDNSConfig() *coral.Command {
|
||||
func setDNSConfigCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "set-dns",
|
||||
Short: "Set DNS config",
|
||||
@@ -76,18 +103,18 @@ func setDNSConfig() *coral.Command {
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "")
|
||||
command.Flags().StringSliceVarP(&nameservers, "nameserver", "", []string{}, "")
|
||||
command.Flags().BoolVarP(&magicDNS, "magic-dns", "", false, "")
|
||||
command.Flags().BoolVarP(&overrideLocalDNS, "override-local-dns", "", false, "")
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
command.Flags().StringSliceVarP(&nameservers, "nameserver", "", []string{}, "Machines on your network will use these nameservers to resolve DNS queries.")
|
||||
command.Flags().BoolVarP(&magicDNS, "magic-dns", "", false, "Enable MagicDNS for the specified Tailnet")
|
||||
command.Flags().BoolVarP(&overrideLocalDNS, "override-local-dns", "", false, "When enabled, connected clients ignore local DNS settings and always use the nameservers specified for this Tailnet")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, c, err := target.createGRPCClient()
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
@@ -116,17 +143,17 @@ func setDNSConfig() *coral.Command {
|
||||
Config: &api.DNSConfig{
|
||||
MagicDns: magicDNS,
|
||||
OverrideLocalDns: overrideLocalDNS,
|
||||
Nameservers: nameservers,
|
||||
Nameservers: globalNameservers,
|
||||
Routes: routes,
|
||||
},
|
||||
}
|
||||
resp, err := client.SetDNSConfig(context.Background(), &req)
|
||||
resp, err := client.SetDNSConfig(context.Background(), connect.NewRequest(&req))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := resp.Config
|
||||
config := resp.Msg.Config
|
||||
|
||||
var allNameservers = config.Nameservers
|
||||
|
||||
|
||||
+33
-33
@@ -3,50 +3,50 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"io"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
apiconnect "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1/ionscalev1connect"
|
||||
"github.com/muesli/coral"
|
||||
)
|
||||
|
||||
func findTailnet(client api.IonscaleClient, tailnet string, tailnetID uint64) (*api.Tailnet, error) {
|
||||
if tailnetID == 0 && tailnet == "" {
|
||||
return nil, fmt.Errorf("requested tailnet not found or you are not authorized for this tailnet")
|
||||
func checkRequiredTailnetAndTailnetIdFlags(cmd *coral.Command, args []string) error {
|
||||
savedTailnetID, err := ionscale.TailnetFromFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnets, err := client.ListTailnets(context.Background(), &api.ListTailnetRequest{})
|
||||
if savedTailnetID == 0 && !cmd.Flags().Changed("tailnet") && !cmd.Flags().Changed("tailnet-id") {
|
||||
return fmt.Errorf("flag --tailnet or --tailnet-id is required")
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("tailnet") && cmd.Flags().Changed("tailnet-id") {
|
||||
return fmt.Errorf("flags --tailnet and --tailnet-id are mutually exclusive")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findTailnet(client apiconnect.IonscaleServiceClient, tailnet string, tailnetID uint64) (*api.Tailnet, error) {
|
||||
savedTailnetID, err := ionscale.TailnetFromFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tailnets.Tailnet {
|
||||
if t.Id == tailnetID || t.Name == tailnet {
|
||||
if savedTailnetID == 0 && tailnetID == 0 && tailnet == "" {
|
||||
return nil, fmt.Errorf("requested tailnet not found or you are not authorized for this tailnet")
|
||||
}
|
||||
|
||||
tailnets, err := client.ListTailnets(context.Background(), connect.NewRequest(&api.ListTailnetRequest{}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tailnets.Msg.Tailnet {
|
||||
if t.Id == savedTailnetID || t.Id == tailnetID || t.Name == tailnet {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("requested tailnet not found or you are not authorized for this tailnet")
|
||||
}
|
||||
|
||||
func findAuthMethod(client api.IonscaleClient, authMethod string, authMethodID uint64) (*api.AuthMethod, error) {
|
||||
if authMethodID == 0 && authMethod == "" {
|
||||
return nil, fmt.Errorf("requested auth method not found or you are not authorized for this tailnet")
|
||||
}
|
||||
|
||||
resp, err := client.ListAuthMethods(context.Background(), &api.ListAuthMethodsRequest{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range resp.AuthMethods {
|
||||
if t.Id == authMethodID || t.Name == authMethod {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("requested auth method not found or you are not authorized for this tailnet")
|
||||
}
|
||||
|
||||
func safeClose(c io.Closer) {
|
||||
if c != nil {
|
||||
_ = c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
func getIAMPolicyCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "get-iam-policy",
|
||||
Short: "Get the IAM policy",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(cmd *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := client.GetIAMPolicy(context.Background(), connect.NewRequest(&api.GetIAMPolicyRequest{TailnetId: tailnet.Id}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
marshal, err := json.MarshalIndent(resp.Msg.Policy, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(marshal))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func setIAMPolicyCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "set-iam-policy",
|
||||
Short: "Set IAM policy",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
var file string
|
||||
var target = Target{}
|
||||
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
command.Flags().StringVar(&file, "file", "", "Path to json file with the acl configuration")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(cmd *coral.Command, args []string) error {
|
||||
rawJson, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var policy = &api.IAMPolicy{}
|
||||
if err := json.Unmarshal(rawJson, policy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.SetIAMPolicy(context.Background(), connect.NewRequest(&api.SetIAMPolicyRequest{TailnetId: tailnet.Id, Policy: policy}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("IAM policy updated successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
+9
-5
@@ -12,13 +12,17 @@ func keyCommand() *coral.Command {
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var disableNewLine bool
|
||||
|
||||
command.Flags().BoolVarP(&disableNewLine, "no-newline", "n", false, "do not output a trailing newline")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
serverKey := key.NewServerKey()
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf(" %s\n", serverKey.String())
|
||||
fmt.Println()
|
||||
|
||||
if disableNewLine {
|
||||
fmt.Print(serverKey.String())
|
||||
} else {
|
||||
fmt.Println(serverKey.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+346
-46
@@ -3,12 +3,15 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"github.com/bufbuild/connect-go"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/nleeper/goment"
|
||||
"github.com/rodaine/table"
|
||||
"inet.af/netaddr"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
func machineCommands() *coral.Command {
|
||||
@@ -18,11 +21,125 @@ func machineCommands() *coral.Command {
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
command.AddCommand(getMachineCommand())
|
||||
command.AddCommand(deleteMachineCommand())
|
||||
command.AddCommand(expireMachineCommand())
|
||||
command.AddCommand(listMachinesCommand())
|
||||
command.AddCommand(getMachineRoutesCommand())
|
||||
command.AddCommand(setMachineRoutesCommand())
|
||||
command.AddCommand(enableMachineRoutesCommand())
|
||||
command.AddCommand(disableMachineRoutesCommand())
|
||||
command.AddCommand(enableMachineKeyExpiryCommand())
|
||||
command.AddCommand(enableExitNodeCommand())
|
||||
command.AddCommand(disableExitNodeCommand())
|
||||
command.AddCommand(disableMachineKeyExpiryCommand())
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func getMachineCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "get",
|
||||
Short: "Retrieve detailed information for a machine",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var machineID uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID.")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := api.GetMachineRequest{MachineId: machineID}
|
||||
resp, err := client.GetMachine(context.Background(), connect.NewRequest(&req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := resp.Msg.Machine
|
||||
var lastSeen = "N/A"
|
||||
var expiresAt = "No expiry"
|
||||
|
||||
if m.LastSeen != nil && !m.LastSeen.AsTime().IsZero() {
|
||||
if mom, err := goment.New(m.LastSeen.AsTime()); err == nil {
|
||||
lastSeen = mom.FromNow()
|
||||
}
|
||||
}
|
||||
|
||||
if !m.KeyExpiryDisabled && m.ExpiresAt != nil && !m.ExpiresAt.AsTime().IsZero() {
|
||||
if mom, err := goment.New(m.ExpiresAt.AsTime()); !m.ExpiresAt.AsTime().IsZero() && err == nil {
|
||||
expiresAt = mom.FromNow()
|
||||
}
|
||||
}
|
||||
|
||||
// initialize tabwriter
|
||||
w := new(tabwriter.Writer)
|
||||
|
||||
// minwidth, tabwidth, padding, padchar, flags
|
||||
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
|
||||
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintf(w, "%s\t%d\n", "ID", m.Id)
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Machine name", m.Name)
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Creator", m.User.Name)
|
||||
fmt.Fprintf(w, "%s\t%s\n", "OS", m.Os)
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Tailscale version", m.ClientVersion)
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Tailscale IPv4", m.Ipv4)
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Tailscale IPv6", m.Ipv6)
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Last seen", lastSeen)
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Key expiry", expiresAt)
|
||||
|
||||
for i, t := range m.Tags {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "ACL tags", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
|
||||
for i, e := range m.ClientConnectivity.Endpoints {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Endpoints", e)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", e)
|
||||
}
|
||||
}
|
||||
|
||||
for i, t := range m.AdvertisedRoutes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Advertised routes", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
|
||||
for i, t := range m.EnabledRoutes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Enabled routes", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
|
||||
if m.AdvertisedExitNode {
|
||||
if m.EnabledExitNode {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "enabled")
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "disabled")
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "no")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -37,17 +154,18 @@ func deleteMachineCommand() *coral.Command {
|
||||
var machineID uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "")
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID.")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, c, err := target.createGRPCClient()
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
req := api.DeleteMachineRequest{MachineId: machineID}
|
||||
if _, err := client.DeleteMachine(context.Background(), &req); err != nil {
|
||||
if _, err := client.DeleteMachine(context.Background(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -69,17 +187,18 @@ func expireMachineCommand() *coral.Command {
|
||||
var machineID uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "")
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID.")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, c, err := target.createGRPCClient()
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
req := api.ExpireMachineRequest{MachineId: machineID}
|
||||
if _, err := client.ExpireMachine(context.Background(), &req); err != nil {
|
||||
if _, err := client.ExpireMachine(context.Background(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -103,15 +222,15 @@ func listMachinesCommand() *coral.Command {
|
||||
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "")
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, c, err := target.createGRPCClient()
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
@@ -119,14 +238,14 @@ func listMachinesCommand() *coral.Command {
|
||||
}
|
||||
|
||||
req := api.ListMachinesRequest{TailnetId: tailnet.Id}
|
||||
resp, err := client.ListMachines(context.Background(), &req)
|
||||
resp, err := client.ListMachines(context.Background(), connect.NewRequest(&req))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tbl := table.New("ID", "TAILNET", "NAME", "IPv4", "IPv6", "EPHEMERAL", "LAST_SEEN", "TAGS")
|
||||
for _, m := range resp.Machines {
|
||||
for _, m := range resp.Msg.Machines {
|
||||
var lastSeen = "N/A"
|
||||
if m.Connected {
|
||||
lastSeen = "Connected"
|
||||
@@ -149,33 +268,30 @@ func listMachinesCommand() *coral.Command {
|
||||
func getMachineRoutesCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "get-routes",
|
||||
Short: "Show the routes of a machine",
|
||||
Short: "Show routes advertised and enabled by a given machine",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var machineID uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "")
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID.")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
grpcClient, c, err := target.createGRPCClient()
|
||||
grpcClient, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
req := api.GetMachineRoutesRequest{MachineId: machineID}
|
||||
resp, err := grpcClient.GetMachineRoutes(context.Background(), &req)
|
||||
resp, err := grpcClient.GetMachineRoutes(context.Background(), connect.NewRequest(&req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tbl := table.New("ROUTE", "ALLOWED")
|
||||
for _, r := range resp.Routes {
|
||||
tbl.AddRow(r.Advertised, r.Allowed)
|
||||
}
|
||||
tbl.Print()
|
||||
printMachinesRoutesResponse(resp.Msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -183,50 +299,234 @@ func getMachineRoutesCommand() *coral.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func setMachineRoutesCommand() *coral.Command {
|
||||
func enableMachineRoutesCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "set-routes",
|
||||
Short: "Enable routes of a machine",
|
||||
Use: "enable-routes",
|
||||
Short: "Enable routes for a given machine",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var machineID uint64
|
||||
var allowedIps []string
|
||||
var routes []string
|
||||
var replace bool
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "")
|
||||
command.Flags().StringSliceVar(&allowedIps, "allowed-ips", []string{}, "")
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
|
||||
command.Flags().StringSliceVar(&routes, "routes", []string{}, "List of routes to enable")
|
||||
command.Flags().BoolVar(&replace, "replace", false, "Replace current enabled routes with this new list")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, c, err := target.createGRPCClient()
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
var prefixes []netaddr.IPPrefix
|
||||
for _, r := range allowedIps {
|
||||
p, err := netaddr.ParseIPPrefix(r)
|
||||
if err != nil {
|
||||
for _, r := range routes {
|
||||
if _, err := netaddr.ParseIPPrefix(r); err != nil {
|
||||
return err
|
||||
}
|
||||
prefixes = append(prefixes, p)
|
||||
}
|
||||
|
||||
req := api.SetMachineRoutesRequest{MachineId: machineID, AllowedIps: allowedIps}
|
||||
resp, err := client.SetMachineRoutes(context.Background(), &req)
|
||||
req := api.EnableMachineRoutesRequest{MachineId: machineID, Routes: routes, Replace: replace}
|
||||
resp, err := client.EnableMachineRoutes(context.Background(), connect.NewRequest(&req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tbl := table.New("ROUTE", "ALLOWED")
|
||||
for _, r := range resp.Routes {
|
||||
tbl.AddRow(r.Advertised, r.Allowed)
|
||||
}
|
||||
tbl.Print()
|
||||
printMachinesRoutesResponse(resp.Msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func disableMachineRoutesCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "disable-routes",
|
||||
Short: "Disable routes for a given machine",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var machineID uint64
|
||||
var routes []string
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
|
||||
command.Flags().StringSliceVar(&routes, "routes", []string{}, "List of routes to enable")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, r := range routes {
|
||||
if _, err := netaddr.ParseIPPrefix(r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
req := api.DisableMachineRoutesRequest{MachineId: machineID, Routes: routes}
|
||||
resp, err := client.DisableMachineRoutes(context.Background(), connect.NewRequest(&req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printMachinesRoutesResponse(resp.Msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func enableExitNodeCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "enable-exit-node",
|
||||
Short: "Enable given machine as an exit node",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var machineID uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := api.EnableExitNodeRequest{MachineId: machineID}
|
||||
resp, err := client.EnableExitNode(context.Background(), connect.NewRequest(&req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printMachinesRoutesResponse(resp.Msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func disableExitNodeCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "disable-exit-node",
|
||||
Short: "Disable given machine as an exit node",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var machineID uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := api.DisableExitNodeRequest{MachineId: machineID}
|
||||
resp, err := client.DisableExitNode(context.Background(), connect.NewRequest(&req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printMachinesRoutesResponse(resp.Msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func enableMachineKeyExpiryCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "enable-key-expiry",
|
||||
Short: "Enable machine key expiry",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
return configureSetMachineKeyExpiryCommand(command, false)
|
||||
}
|
||||
|
||||
func disableMachineKeyExpiryCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "disable-key-expiry",
|
||||
Short: "Disable machine key expiry",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
return configureSetMachineKeyExpiryCommand(command, true)
|
||||
}
|
||||
|
||||
func configureSetMachineKeyExpiryCommand(command *coral.Command, v bool) *coral.Command {
|
||||
var machineID uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
|
||||
|
||||
_ = command.MarkFlagRequired("machine-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := api.SetMachineKeyExpiryRequest{MachineId: machineID, Disabled: v}
|
||||
_, err = client.SetMachineKeyExpiry(context.Background(), connect.NewRequest(&req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func printMachinesRoutesResponse(msg *api.GetMachineRoutesResponse) {
|
||||
w := new(tabwriter.Writer)
|
||||
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
|
||||
defer w.Flush()
|
||||
|
||||
for i, t := range msg.AdvertisedRoutes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Advertised routes", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
|
||||
for i, t := range msg.EnabledRoutes {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Enabled routes", t)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "", t)
|
||||
}
|
||||
}
|
||||
|
||||
if msg.AdvertisedExitNode {
|
||||
if msg.EnabledExitNode {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "enabled")
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "disabled")
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "no")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,16 @@ import (
|
||||
|
||||
func Command() *coral.Command {
|
||||
rootCmd := rootCommand()
|
||||
rootCmd.AddCommand(configureCommand())
|
||||
rootCmd.AddCommand(keyCommand())
|
||||
rootCmd.AddCommand(authCommand())
|
||||
rootCmd.AddCommand(derpMapCommand())
|
||||
rootCmd.AddCommand(serverCommand())
|
||||
rootCmd.AddCommand(versionCommand())
|
||||
rootCmd.AddCommand(authMethodsCommand())
|
||||
rootCmd.AddCommand(authFilterCommand())
|
||||
rootCmd.AddCommand(tailnetCommand())
|
||||
rootCmd.AddCommand(authkeysCommand())
|
||||
rootCmd.AddCommand(machineCommands())
|
||||
rootCmd.AddCommand(userCommands())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ func serverCommand() *coral.Command {
|
||||
|
||||
var configFile string
|
||||
|
||||
command.Flags().StringVarP(&configFile, "config", "c", "ionscale.yaml", "Path to the configuration file.")
|
||||
command.Flags().StringVarP(&configFile, "config", "c", "", "Path to the configuration file.")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
|
||||
|
||||
+67
-24
@@ -3,25 +3,29 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"github.com/bufbuild/connect-go"
|
||||
idomain "github.com/jsiebens/ionscale/internal/domain"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/rodaine/table"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func tailnetCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "tailnets",
|
||||
Short: "Manage ionscale tailnets",
|
||||
Long: "This command allows operations on ionscale tailnet resources.",
|
||||
}
|
||||
|
||||
command.AddCommand(listTailnetsCommand())
|
||||
command.AddCommand(createTailnetsCommand())
|
||||
command.AddCommand(deleteTailnetCommand())
|
||||
command.AddCommand(getDNSConfig())
|
||||
command.AddCommand(setDNSConfig())
|
||||
command.AddCommand(getACLConfig())
|
||||
command.AddCommand(setACLConfig())
|
||||
command.AddCommand(getDNSConfigCommand())
|
||||
command.AddCommand(setDNSConfigCommand())
|
||||
command.AddCommand(getACLConfigCommand())
|
||||
command.AddCommand(setACLConfigCommand())
|
||||
command.AddCommand(getIAMPolicyCommand())
|
||||
command.AddCommand(setIAMPolicyCommand())
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -29,8 +33,7 @@ func tailnetCommand() *coral.Command {
|
||||
func listTailnetsCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "list",
|
||||
Short: "List tailnets",
|
||||
Long: `List tailnets in this ionscale instance.`,
|
||||
Short: "List available Tailnets",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
@@ -39,20 +42,19 @@ func listTailnetsCommand() *coral.Command {
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
|
||||
client, c, err := target.createGRPCClient()
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
resp, err := client.ListTailnets(context.Background(), &api.ListTailnetRequest{})
|
||||
resp, err := client.ListTailnets(context.Background(), connect.NewRequest(&api.ListTailnetRequest{}))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tbl := table.New("ID", "NAME")
|
||||
for _, tailnet := range resp.Tailnet {
|
||||
for _, tailnet := range resp.Msg.Tailnet {
|
||||
tbl.AddRow(tailnet.Id, tailnet.Name)
|
||||
}
|
||||
tbl.Print()
|
||||
@@ -66,33 +68,74 @@ func listTailnetsCommand() *coral.Command {
|
||||
func createTailnetsCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new tailnet",
|
||||
Short: "Create a new Tailnet",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var name string
|
||||
var domain string
|
||||
var email string
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.Flags().StringVarP(&name, "name", "n", "", "")
|
||||
_ = command.MarkFlagRequired("name")
|
||||
command.Flags().StringVar(&domain, "domain", "", "")
|
||||
command.Flags().StringVar(&email, "email", "", "")
|
||||
|
||||
command.PreRunE = func(cmd *coral.Command, args []string) error {
|
||||
if name == "" && email == "" && domain == "" {
|
||||
return fmt.Errorf("at least flag --name, --email or --domain is required")
|
||||
}
|
||||
if domain != "" && email != "" {
|
||||
return fmt.Errorf("flags --email and --domain are mutually exclusive")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
|
||||
client, c, err := target.createGRPCClient()
|
||||
var tailnetName = ""
|
||||
var iamPolicy = api.IAMPolicy{}
|
||||
|
||||
if len(domain) != 0 {
|
||||
domainToLower := strings.ToLower(domain)
|
||||
tailnetName = domainToLower
|
||||
iamPolicy = api.IAMPolicy{
|
||||
Filters: []string{fmt.Sprintf("domain == %s", domainToLower)},
|
||||
}
|
||||
}
|
||||
|
||||
if len(email) != 0 {
|
||||
emailToLower := strings.ToLower(email)
|
||||
tailnetName = emailToLower
|
||||
iamPolicy = api.IAMPolicy{
|
||||
Emails: []string{emailToLower},
|
||||
Roles: map[string]string{
|
||||
emailToLower: string(idomain.UserRoleAdmin),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if len(name) != 0 {
|
||||
tailnetName = name
|
||||
}
|
||||
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
resp, err := client.CreateTailnet(context.Background(), &api.CreateTailnetRequest{Name: name})
|
||||
resp, err := client.CreateTailnet(context.Background(), connect.NewRequest(&api.CreateTailnetRequest{
|
||||
Name: tailnetName,
|
||||
IamPolicy: &iamPolicy,
|
||||
}))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tbl := table.New("ID", "NAME")
|
||||
tbl.AddRow(resp.Tailnet.Id, resp.Tailnet.Name)
|
||||
tbl.AddRow(resp.Msg.Tailnet.Id, resp.Msg.Tailnet.Name)
|
||||
tbl.Print()
|
||||
|
||||
return nil
|
||||
@@ -114,24 +157,24 @@ func deleteTailnetCommand() *coral.Command {
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "")
|
||||
command.Flags().BoolVar(&force, "force", false, "")
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
command.Flags().BoolVar(&force, "force", false, "When enabled, force delete the specified Tailnet even when machines are still available.")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
|
||||
client, c, err := target.createGRPCClient()
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.DeleteTailnet(context.Background(), &api.DeleteTailnetRequest{TailnetId: tailnet.Id, Force: force})
|
||||
_, err = client.DeleteTailnet(context.Background(), connect.NewRequest(&api.DeleteTailnetRequest{TailnetId: tailnet.Id, Force: force}))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+7
-19
@@ -3,21 +3,18 @@ package cmd
|
||||
import (
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1/ionscalev1connect"
|
||||
"github.com/muesli/coral"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
ionscaleSystemAdminKey = "IONSCALE_ADMIN_KEY"
|
||||
ionscaleSystemAdminKey = "IONSCALE_SYSTEM_ADMIN_KEY"
|
||||
ionscaleAddr = "IONSCALE_ADDR"
|
||||
ionscaleInsecureSkipVerify = "IONSCALE_SKIP_VERIFY"
|
||||
ionscaleUseGrpcWeb = "IONSCALE_GRPC_WEB"
|
||||
)
|
||||
|
||||
type Target struct {
|
||||
addr string
|
||||
useGrpcWeb bool
|
||||
insecureSkipVerify bool
|
||||
systemAdminKey string
|
||||
}
|
||||
@@ -25,29 +22,27 @@ type Target struct {
|
||||
func (t *Target) prepareCommand(cmd *coral.Command) {
|
||||
cmd.Flags().StringVar(&t.addr, "addr", "", "Addr of the ionscale server, as a complete URL")
|
||||
cmd.Flags().BoolVar(&t.insecureSkipVerify, "tls-skip-verify", false, "Disable verification of TLS certificates")
|
||||
cmd.Flags().BoolVar(&t.useGrpcWeb, "grpc-web", false, "Enables gRPC-web protocol. Useful if ionscale server is behind proxy which does not support GRPC")
|
||||
cmd.Flags().StringVar(&t.systemAdminKey, "admin-key", "", "If specified, the given value will be used as the key to generate a Bearer token for the call. This can also be specified via the IONSCALE_ADMIN_KEY environment variable.")
|
||||
cmd.Flags().StringVar(&t.systemAdminKey, "system-admin-key", "", "If specified, the given value will be used as the key to generate a Bearer token for the call. This can also be specified via the IONSCALE_ADMIN_KEY environment variable.")
|
||||
}
|
||||
|
||||
func (t *Target) createGRPCClient() (api.IonscaleClient, io.Closer, error) {
|
||||
func (t *Target) createGRPCClient() (api.IonscaleServiceClient, error) {
|
||||
addr := t.getAddr()
|
||||
useGrpcWeb := t.getUseGrpcWeb()
|
||||
skipVerify := t.getInsecureSkipVerify()
|
||||
systemAdminKey := t.getSystemAdminKey()
|
||||
|
||||
auth, err := ionscale.LoadClientAuth(systemAdminKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ionscale.NewClient(auth, addr, skipVerify, useGrpcWeb)
|
||||
return ionscale.NewClient(auth, addr, skipVerify)
|
||||
}
|
||||
|
||||
func (t *Target) getAddr() string {
|
||||
if len(t.addr) != 0 {
|
||||
return t.addr
|
||||
}
|
||||
return config.GetString(ionscaleAddr, "https://localhost:8000")
|
||||
return config.GetString(ionscaleAddr, "https://localhost:8443")
|
||||
}
|
||||
|
||||
func (t *Target) getInsecureSkipVerify() bool {
|
||||
@@ -57,13 +52,6 @@ func (t *Target) getInsecureSkipVerify() bool {
|
||||
return config.GetBool(ionscaleInsecureSkipVerify, false)
|
||||
}
|
||||
|
||||
func (t *Target) getUseGrpcWeb() bool {
|
||||
if t.useGrpcWeb {
|
||||
return true
|
||||
}
|
||||
return config.GetBool(ionscaleUseGrpcWeb, false)
|
||||
}
|
||||
|
||||
func (t *Target) getSystemAdminKey() string {
|
||||
if len(t.systemAdminKey) != 0 {
|
||||
return t.systemAdminKey
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
"github.com/rodaine/table"
|
||||
)
|
||||
|
||||
func userCommands() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "users",
|
||||
Short: "Manage ionscale users",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
command.AddCommand(listUsersCommand())
|
||||
command.AddCommand(deleteUserCommand())
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func listUsersCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "list",
|
||||
Short: "List users",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var tailnetID uint64
|
||||
var tailnetName string
|
||||
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().StringVar(&tailnetName, "tailnet", "", "Tailnet name. Mutually exclusive with --tailnet-id.")
|
||||
command.Flags().Uint64Var(&tailnetID, "tailnet-id", 0, "Tailnet ID. Mutually exclusive with --tailnet.")
|
||||
|
||||
command.PreRunE = checkRequiredTailnetAndTailnetIdFlags
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnet, err := findTailnet(client, tailnetName, tailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := api.ListUsersRequest{TailnetId: tailnet.Id}
|
||||
resp, err := client.ListUsers(context.Background(), connect.NewRequest(&req))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tbl := table.New("ID", "USER", "ROLE")
|
||||
for _, m := range resp.Msg.Users {
|
||||
tbl.AddRow(m.Id, m.Name, m.Role)
|
||||
}
|
||||
tbl.Print()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func deleteUserCommand() *coral.Command {
|
||||
command := &coral.Command{
|
||||
Use: "delete",
|
||||
Short: "Deletes a user",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var userID uint64
|
||||
var target = Target{}
|
||||
target.prepareCommand(command)
|
||||
command.Flags().Uint64Var(&userID, "user-id", 0, "User ID.")
|
||||
|
||||
_ = command.MarkFlagRequired("user-id")
|
||||
|
||||
command.RunE = func(command *coral.Command, args []string) error {
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := api.DeleteUserRequest{UserId: userID}
|
||||
if _, err := client.DeleteUser(context.Background(), connect.NewRequest(&req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("User deleted.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -3,8 +3,9 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/version"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/muesli/coral"
|
||||
)
|
||||
|
||||
@@ -26,7 +27,7 @@ Client:
|
||||
Git Revision: %s
|
||||
`, clientVersion, clientRevision)
|
||||
|
||||
client, c, err := target.createGRPCClient()
|
||||
client, err := target.createGRPCClient()
|
||||
if err != nil {
|
||||
fmt.Printf(`
|
||||
Server:
|
||||
@@ -34,9 +35,8 @@ Server:
|
||||
`, err)
|
||||
return
|
||||
}
|
||||
defer safeClose(c)
|
||||
|
||||
resp, err := client.GetVersion(context.Background(), &api.GetVersionRequest{})
|
||||
resp, err := client.GetVersion(context.Background(), connect.NewRequest(&api.GetVersionRequest{}))
|
||||
if err != nil {
|
||||
fmt.Printf(`
|
||||
Server:
|
||||
@@ -50,7 +50,7 @@ Server:
|
||||
Addr: %s
|
||||
Version: %s
|
||||
Git Revision: %s
|
||||
`, target.getAddr(), resp.Version, resp.Revision)
|
||||
`, target.getAddr(), resp.Msg.Version, resp.Msg.Revision)
|
||||
|
||||
}
|
||||
|
||||
|
||||
+149
-73
@@ -2,123 +2,176 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/caarlos0/env/v6"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/key"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
tkey "tailscale.com/types/key"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultKeepAliveInterval = 1 * time.Minute
|
||||
defaultMagicDNSSuffix = "ionscale.net"
|
||||
)
|
||||
|
||||
var (
|
||||
keepAliveInterval = defaultKeepAliveInterval
|
||||
magicDNSSuffix = defaultMagicDNSSuffix
|
||||
)
|
||||
|
||||
func KeepAliveInterval() time.Duration {
|
||||
return keepAliveInterval
|
||||
}
|
||||
|
||||
func MagicDNSSuffix() string {
|
||||
return magicDNSSuffix
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
config := defaultConfig()
|
||||
cfg := defaultConfig()
|
||||
|
||||
if len(path) != 0 {
|
||||
expandedPath, err := homedir.Expand(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err := ioutil.ReadFile(expandedPath)
|
||||
|
||||
absPath, err := filepath.Abs(expandedPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(b, config); err != nil {
|
||||
b, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(b, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
envCfg := &Config{}
|
||||
if err := env.Parse(envCfg, env.Options{Prefix: "IONSCALE_"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
const (
|
||||
httpListenAddrKey = "IONSCALE_HTTP_LISTEN_ADDR"
|
||||
httpsListenAddrKey = "IONSCALE_HTTPS_LISTEN_ADDR"
|
||||
serverUrlKey = "IONSCALE_SERVER_URL"
|
||||
keysSystemAdminKeyKey = "IONSCALE_SYSTEM_ADMIN_KEY"
|
||||
keysEncryptionKeyKey = "IONSCALE_ENCRYPTION_KEY"
|
||||
databaseUrlKey = "IONSCALE_DB_URL"
|
||||
tlsDisableKey = "IONSCALE_TLS_DISABLE"
|
||||
tlsCertFileKey = "IONSCALE_TLS_CERT_FILE"
|
||||
tlsKeyFileKey = "IONSCALE_TLS_KEY_FILE"
|
||||
tlsCertMagicCAKey = "IONSCALE_TLS_CERT_MAGIC_CA"
|
||||
tlsCertMagicDomainKey = "IONSCALE_TLS_CERT_MAGIC_DOMAIN"
|
||||
tlsCertMagicEmailKey = "IONSCALE_TLS_CERT_MAGIC_EMAIL"
|
||||
tlsCertMagicStoragePath = "IONSCALE_TLS_CERT_MAGIC_STORAGE_PATH"
|
||||
metricsListenAddrKey = "IONSCALE_METRICS_LISTEN_ADDR"
|
||||
loggingLevelKey = "IONSCALE_LOGGING_LEVEL"
|
||||
loggingFormatKey = "IONSCALE_LOGGING_FORMAT"
|
||||
loggingFileKey = "IONSCALE_LOGGING_FILE"
|
||||
)
|
||||
// merge env configuration on top of the default/file configuration
|
||||
if err := mergo.Merge(cfg, envCfg, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keepAliveInterval = cfg.PollNet.KeepAliveInterval
|
||||
magicDNSSuffix = cfg.DNS.MagicDNSSuffix
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func defaultConfig() *Config {
|
||||
return &Config{
|
||||
HttpListenAddr: GetString(httpListenAddrKey, ":8080"),
|
||||
HttpsListenAddr: GetString(httpsListenAddrKey, ":8443"),
|
||||
MetricsListenAddr: GetString(metricsListenAddrKey, ":8081"),
|
||||
ServerUrl: GetString(serverUrlKey, "https://localhost:8443"),
|
||||
Keys: Keys{
|
||||
SystemAdminKey: GetString(keysSystemAdminKeyKey, ""),
|
||||
EncryptionKey: GetString(keysEncryptionKeyKey, ""),
|
||||
},
|
||||
HttpListenAddr: ":8080",
|
||||
HttpsListenAddr: ":8443",
|
||||
MetricsListenAddr: ":9091",
|
||||
ServerUrl: "https://localhost:8843",
|
||||
Database: Database{
|
||||
Url: GetString(databaseUrlKey, "ionscale.db"),
|
||||
Type: "sqlite",
|
||||
Url: "./ionscale.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)",
|
||||
},
|
||||
Tls: Tls{
|
||||
Disable: GetBool(tlsDisableKey, false),
|
||||
CertFile: GetString(tlsCertFileKey, ""),
|
||||
KeyFile: GetString(tlsKeyFileKey, ""),
|
||||
CertMagicCA: GetString(tlsCertMagicCAKey, certmagic.LetsEncryptProductionCA),
|
||||
CertMagicDomain: GetString(tlsCertMagicDomainKey, ""),
|
||||
CertMagicEmail: GetString(tlsCertMagicEmailKey, ""),
|
||||
CertMagicStoragePath: GetString(tlsCertMagicStoragePath, ""),
|
||||
Disable: false,
|
||||
ForceHttps: true,
|
||||
AcmeEnabled: false,
|
||||
AcmeCA: certmagic.LetsEncryptProductionCA,
|
||||
AcmePath: "./acme",
|
||||
},
|
||||
PollNet: PollNet{
|
||||
KeepAliveInterval: defaultKeepAliveInterval,
|
||||
},
|
||||
DNS: DNS{
|
||||
MagicDNSSuffix: defaultMagicDNSSuffix,
|
||||
},
|
||||
Logging: Logging{
|
||||
Level: GetString(loggingLevelKey, "info"),
|
||||
Format: GetString(loggingFormatKey, ""),
|
||||
File: GetString(loggingFileKey, ""),
|
||||
Level: "info",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type ServerKeys struct {
|
||||
SystemAdminKey key.ServerPrivate
|
||||
SystemAdminKey *key.ServerPrivate
|
||||
ControlKey tkey.MachinePrivate
|
||||
LegacyControlKey tkey.MachinePrivate
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
HttpListenAddr string `yaml:"http_listen_addr,omitempty"`
|
||||
HttpsListenAddr string `yaml:"https_listen_addr,omitempty"`
|
||||
MetricsListenAddr string `yaml:"metrics_listen_addr,omitempty"`
|
||||
ServerUrl string `yaml:"server_url,omitempty"`
|
||||
Tls Tls `yaml:"tls,omitempty"`
|
||||
Logging Logging `yaml:"logging,omitempty"`
|
||||
Keys Keys `yaml:"keys,omitempty"`
|
||||
Database Database `yaml:"database,omitempty"`
|
||||
HttpListenAddr string `yaml:"http_listen_addr,omitempty" env:"HTTP_LISTEN_ADDR"`
|
||||
HttpsListenAddr string `yaml:"https_listen_addr,omitempty" env:"HTTPS_LISTEN_ADDR"`
|
||||
MetricsListenAddr string `yaml:"metrics_listen_addr,omitempty" env:"METRICS_LISTEN_ADDR"`
|
||||
ServerUrl string `yaml:"server_url,omitempty" env:"SERVER_URL"`
|
||||
Tls Tls `yaml:"tls,omitempty" envPrefix:"TLS_"`
|
||||
PollNet PollNet `yaml:"poll_net,omitempty" envPrefix:"POLL_NET_"`
|
||||
Keys Keys `yaml:"keys,omitempty" envPrefix:"KEYS_"`
|
||||
Database Database `yaml:"database,omitempty" envPrefix:"DB_"`
|
||||
AuthProvider AuthProvider `yaml:"auth_provider,omitempty"`
|
||||
DNS DNS `yaml:"dns,omitempty"`
|
||||
Logging Logging `yaml:"logging,omitempty" envPrefix:"LOGGING_"`
|
||||
}
|
||||
|
||||
type Tls struct {
|
||||
Disable bool `yaml:"disable"`
|
||||
CertFile string `yaml:"cert_file,omitempty"`
|
||||
KeyFile string `yaml:"key_file,omitempty"`
|
||||
CertMagicDomain string `yaml:"cert_magic_domain,omitempty"`
|
||||
CertMagicEmail string `yaml:"cert_magic_email,omitempty"`
|
||||
CertMagicCA string `yaml:"cert_magic_ca,omitempty"`
|
||||
CertMagicStoragePath string `yaml:"cert_magic_storage_path,omitempty"`
|
||||
Disable bool `yaml:"disable" env:"DISABLE"`
|
||||
ForceHttps bool `yaml:"force_https" env:"FORCE_HTTPS"`
|
||||
CertFile string `yaml:"cert_file,omitempty" env:"CERT_FILE"`
|
||||
KeyFile string `yaml:"key_file,omitempty" env:"KEY_FILE"`
|
||||
AcmeEnabled bool `yaml:"acme,omitempty" env:"ACME_ENABLED"`
|
||||
AcmeEmail string `yaml:"acme_email,omitempty" env:"ACME_EMAIL"`
|
||||
AcmeCA string `yaml:"acme_ca,omitempty" env:"ACME_CA"`
|
||||
AcmePath string `yaml:"acme_path,omitempty" env:"ACME_PATH"`
|
||||
}
|
||||
|
||||
type PollNet struct {
|
||||
KeepAliveInterval time.Duration `yaml:"keep_alive_interval" env:"KEEP_ALIVE_INTERVAL"`
|
||||
}
|
||||
|
||||
type Logging struct {
|
||||
Level string `yaml:"level,omitempty"`
|
||||
Format string `yaml:"format,omitempty"`
|
||||
File string `yaml:"file,omitempty"`
|
||||
Level string `yaml:"level,omitempty" env:"LEVEL"`
|
||||
Format string `yaml:"format,omitempty" env:"FORMAT"`
|
||||
File string `yaml:"file,omitempty" env:"FILE"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
Url string `yaml:"url,omitempty"`
|
||||
Type string `yaml:"type,omitempty" env:"TYPE"`
|
||||
Url string `yaml:"url,omitempty" env:"URL"`
|
||||
}
|
||||
|
||||
type Keys struct {
|
||||
SystemAdminKey string `yaml:"system_admin_key,omitempty"`
|
||||
EncryptionKey string `yaml:"encryption_key,omitempty"`
|
||||
ControlKey string `yaml:"control_key,omitempty" env:"CONTROL_KEY"`
|
||||
LegacyControlKey string `yaml:"legacy_control_key,omitempty" env:"LEGACY_CONTROL_KEY"`
|
||||
SystemAdminKey string `yaml:"system_admin_key,omitempty" env:"SYSTEM_ADMIN_KEY"`
|
||||
}
|
||||
|
||||
type AuthProvider struct {
|
||||
Issuer string `yaml:"issuer"`
|
||||
ClientID string `yaml:"client_id"`
|
||||
ClientSecret string `yaml:"client_secret"`
|
||||
Scopes []string `yaml:"additional_scopes"`
|
||||
SystemAdminPolicy SystemAdminPolicy `yaml:"system_admins"`
|
||||
}
|
||||
|
||||
type DNS struct {
|
||||
MagicDNSSuffix string `yaml:"magic_dns_suffix"`
|
||||
}
|
||||
|
||||
type SystemAdminPolicy struct {
|
||||
Subs []string `json:"subs,omitempty"`
|
||||
Emails []string `json:"emails,omitempty"`
|
||||
Filters []string `json:"filters,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Config) CreateUrl(format string, a ...interface{}) string {
|
||||
@@ -126,13 +179,36 @@ func (c *Config) CreateUrl(format string, a ...interface{}) string {
|
||||
return strings.TrimSuffix(c.ServerUrl, "/") + "/" + strings.TrimPrefix(path, "/")
|
||||
}
|
||||
|
||||
func (c *Config) ReadServerKeys() (*ServerKeys, error) {
|
||||
systemAdminKey, err := key.ParsePrivateKey(c.Keys.SystemAdminKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading system admin key: %v", err)
|
||||
func (c *Config) ReadServerKeys(defaultKeys *domain.ControlKeys) (*ServerKeys, error) {
|
||||
keys := &ServerKeys{
|
||||
ControlKey: defaultKeys.ControlKey,
|
||||
LegacyControlKey: defaultKeys.LegacyControlKey,
|
||||
}
|
||||
|
||||
return &ServerKeys{
|
||||
SystemAdminKey: *systemAdminKey,
|
||||
}, nil
|
||||
if len(c.Keys.SystemAdminKey) != 0 {
|
||||
systemAdminKey, err := key.ParsePrivateKey(c.Keys.SystemAdminKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading system admin key: %v", err)
|
||||
}
|
||||
|
||||
keys.SystemAdminKey = systemAdminKey
|
||||
}
|
||||
|
||||
if len(c.Keys.ControlKey) != 0 {
|
||||
controlKey, err := util.ParseMachinePrivateKey(c.Keys.ControlKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading control key: %v", err)
|
||||
}
|
||||
keys.ControlKey = *controlKey
|
||||
}
|
||||
|
||||
if len(c.Keys.LegacyControlKey) != 0 {
|
||||
legacyControlKey, err := util.ParseMachinePrivateKey(c.Keys.LegacyControlKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading legacy control key: %v", err)
|
||||
}
|
||||
keys.LegacyControlKey = *legacyControlKey
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/glebarez/sqlite"
|
||||
"fmt"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"net/http"
|
||||
"tailscale.com/tailcfg"
|
||||
tskey "tailscale.com/types/key"
|
||||
"github.com/jsiebens/ionscale/internal/broker"
|
||||
"github.com/jsiebens/ionscale/internal/database/migration"
|
||||
"tailscale.com/types/key"
|
||||
"time"
|
||||
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
@@ -17,106 +17,97 @@ import (
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func OpenDB(config *config.Database, logger hclog.Logger) (*gorm.DB, domain.Repository, error) {
|
||||
gormDB, err := createDB(config, logger)
|
||||
type db interface {
|
||||
DB() *gorm.DB
|
||||
Lock() error
|
||||
Unlock() error
|
||||
UnlockErr(error) error
|
||||
}
|
||||
|
||||
func OpenDB(config *config.Database, logger hclog.Logger) (domain.Repository, broker.Pubsub, error) {
|
||||
db, pubsub, err := createDB(config, logger)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
repository := domain.NewRepository(gormDB)
|
||||
repository := domain.NewRepository(db.DB())
|
||||
|
||||
if err := migrate(gormDB, repository); err != nil {
|
||||
if err := db.Lock(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return gormDB, repository, nil
|
||||
if err := db.UnlockErr(migrate(db.DB())); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return repository, pubsub, nil
|
||||
}
|
||||
|
||||
func createDB(config *config.Database, logger hclog.Logger) (*gorm.DB, error) {
|
||||
func createDB(config *config.Database, logger hclog.Logger) (db, broker.Pubsub, error) {
|
||||
gormConfig := &gorm.Config{
|
||||
Logger: &GormLoggerAdapter{logger: logger.Named("db")},
|
||||
}
|
||||
|
||||
return gorm.Open(sqlite.Open(config.Url), gormConfig)
|
||||
switch config.Type {
|
||||
case "sqlite", "sqlite3":
|
||||
db, err := newSqliteDB(config, gormConfig)
|
||||
return db, broker.NewPubsubInMemory(), err
|
||||
case "postgres", "postgresql":
|
||||
db, err := newPostgresDB(config, gormConfig)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
stdDB, err := db.DB().DB()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pubsub, err := broker.NewPubsub(context.TODO(), stdDB, config.Url)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return db, pubsub, err
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("invalid database type '%s'", config.Type)
|
||||
}
|
||||
|
||||
func migrate(db *gorm.DB, repository domain.Repository) error {
|
||||
err := db.AutoMigrate(
|
||||
&domain.ServerConfig{},
|
||||
&domain.Tailnet{},
|
||||
&domain.TailnetConfig{},
|
||||
&domain.AuthMethod{},
|
||||
&domain.AuthFilter{},
|
||||
&domain.Account{},
|
||||
&domain.User{},
|
||||
&domain.AuthKey{},
|
||||
&domain.Machine{},
|
||||
&domain.RegistrationRequest{},
|
||||
)
|
||||
func migrate(db *gorm.DB) error {
|
||||
m := gormigrate.New(db, gormigrate.DefaultOptions, migration.Migrations())
|
||||
|
||||
if err != nil {
|
||||
if err := m.Migrate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := initializeControlKeys(repository); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
repository := domain.NewRepository(db)
|
||||
|
||||
if err := initializeDERPMap(repository); err != nil {
|
||||
if err := createServerKey(ctx, repository); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initializeDERPMap(repository domain.Repository) error {
|
||||
ctx := context.Background()
|
||||
derpMap, err := repository.GetDERPMap(ctx)
|
||||
func createServerKey(ctx context.Context, repository domain.Repository) error {
|
||||
serverKey, err := repository.GetControlKeys(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if derpMap != nil {
|
||||
if serverKey != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
getJson := func(url string, target interface{}) error {
|
||||
c := http.Client{Timeout: 5 * time.Second}
|
||||
r, err := c.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
return json.NewDecoder(r.Body).Decode(target)
|
||||
keys := domain.ControlKeys{
|
||||
ControlKey: key.NewMachine(),
|
||||
LegacyControlKey: key.NewMachine(),
|
||||
}
|
||||
|
||||
m := &tailcfg.DERPMap{}
|
||||
if err := getJson("https://controlplane.tailscale.com/derpmap/default", m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := repository.SetDERPMap(ctx, m); err != nil {
|
||||
if err := repository.SetControlKeys(ctx, &keys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initializeControlKeys(repository domain.Repository) error {
|
||||
ctx := context.Background()
|
||||
keys, err := repository.GetControlKeys(ctx)
|
||||
if keys != nil || err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keys = &domain.ControlKeys{
|
||||
ControlKey: tskey.NewMachine(),
|
||||
LegacyControlKey: tskey.NewMachine(),
|
||||
}
|
||||
|
||||
return repository.SetControlKeys(ctx, keys)
|
||||
}
|
||||
|
||||
type GormLoggerAdapter struct {
|
||||
logger hclog.Logger
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
func m202209070900_initial_schema() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202209070900",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
// it's a good practice to copy the struct inside the function,
|
||||
// so side effects are prevented if the original struct changes during the time
|
||||
type ServerConfig struct {
|
||||
Key string `gorm:"primary_key"`
|
||||
Value []byte
|
||||
}
|
||||
|
||||
type Tailnet struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Name string `gorm:"type:varchar(64);unique_index"`
|
||||
DNSConfig domain.DNSConfig
|
||||
IAMPolicy domain.IAMPolicy
|
||||
ACLPolicy domain.ACLPolicy
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
ExternalID string
|
||||
LoginName string
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Name string
|
||||
UserType domain.UserType
|
||||
TailnetID uint64
|
||||
Tailnet Tailnet
|
||||
AccountID *uint64
|
||||
Account *Account
|
||||
}
|
||||
|
||||
type SystemApiKey struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Key string `gorm:"type:varchar(64);unique_index"`
|
||||
Hash string
|
||||
|
||||
CreatedAt time.Time
|
||||
ExpiresAt *time.Time
|
||||
|
||||
AccountID uint64
|
||||
Account Account
|
||||
}
|
||||
|
||||
type ApiKey struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Key string `gorm:"type:varchar(64);unique_index"`
|
||||
Hash string
|
||||
|
||||
CreatedAt time.Time
|
||||
ExpiresAt *time.Time
|
||||
|
||||
TailnetID uint64
|
||||
Tailnet Tailnet
|
||||
|
||||
UserID uint64
|
||||
User User
|
||||
}
|
||||
|
||||
type AuthKey struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Key string `gorm:"type:varchar(64);unique_index"`
|
||||
Hash string
|
||||
Ephemeral bool
|
||||
Tags domain.Tags
|
||||
|
||||
CreatedAt time.Time
|
||||
ExpiresAt *time.Time
|
||||
|
||||
TailnetID uint64
|
||||
Tailnet Tailnet
|
||||
|
||||
UserID uint64
|
||||
User User
|
||||
}
|
||||
|
||||
type Machine struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Name string
|
||||
NameIdx uint64
|
||||
MachineKey string
|
||||
NodeKey string
|
||||
DiscoKey string
|
||||
Ephemeral bool
|
||||
RegisteredTags domain.Tags
|
||||
Tags domain.Tags
|
||||
KeyExpiryDisabled bool
|
||||
|
||||
HostInfo domain.HostInfo
|
||||
Endpoints domain.Endpoints
|
||||
AllowIPs domain.AllowIPs
|
||||
|
||||
IPv4 domain.IP
|
||||
IPv6 domain.IP
|
||||
|
||||
CreatedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
LastSeen *time.Time
|
||||
|
||||
UserID uint64
|
||||
User User
|
||||
|
||||
TailnetID uint64
|
||||
Tailnet Tailnet
|
||||
}
|
||||
|
||||
type RegistrationRequest struct {
|
||||
MachineKey string `gorm:"primary_key;autoIncrement:false"`
|
||||
Key string `gorm:"type:varchar(64);unique_index"`
|
||||
Data domain.RegistrationRequestData
|
||||
CreatedAt time.Time
|
||||
Authenticated bool
|
||||
Error string
|
||||
}
|
||||
|
||||
type AuthenticationRequest struct {
|
||||
Key string `gorm:"primary_key;autoIncrement:false"`
|
||||
Token string
|
||||
TailnetID *uint64
|
||||
Error string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
return db.AutoMigrate(
|
||||
&ServerConfig{},
|
||||
&Tailnet{},
|
||||
&Account{},
|
||||
&User{},
|
||||
&SystemApiKey{},
|
||||
&ApiKey{},
|
||||
&AuthKey{},
|
||||
&Machine{},
|
||||
&RegistrationRequest{},
|
||||
&AuthenticationRequest{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func m202209251530_add_autoallowips_column() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202209251530",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type Machine struct {
|
||||
AutoAllowIPs domain.AllowIPs
|
||||
}
|
||||
|
||||
return db.AutoMigrate(
|
||||
&Machine{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
)
|
||||
|
||||
func Migrations() []*gormigrate.Migration {
|
||||
var migrations = []*gormigrate.Migration{
|
||||
m202209070900_initial_schema(),
|
||||
m202209251530_add_autoallowips_column(),
|
||||
}
|
||||
return migrations
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func newPostgresDB(config *config.Database, g *gorm.Config) (db, error) {
|
||||
db, err := gorm.Open(postgres.Open(config.Url), g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Postgres{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Postgres struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (s *Postgres) DB() *gorm.DB {
|
||||
return s.db
|
||||
}
|
||||
|
||||
func (s *Postgres) Lock() error {
|
||||
d, _ := s.db.DB()
|
||||
|
||||
query := `SELECT pg_advisory_lock($1)`
|
||||
id := s.generateAdvisoryLockId()
|
||||
if _, err := d.ExecContext(context.Background(), query, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Postgres) Unlock() error {
|
||||
d, _ := s.db.DB()
|
||||
|
||||
query := `SELECT pg_advisory_unlock($1)`
|
||||
if _, err := d.ExecContext(context.Background(), query, s.generateAdvisoryLockId()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Postgres) UnlockErr(prevErr error) error {
|
||||
if err := s.Unlock(); err != nil {
|
||||
return multierror.Append(prevErr, err)
|
||||
}
|
||||
return prevErr
|
||||
}
|
||||
|
||||
const advisoryLockIDSalt uint = 1486364155
|
||||
|
||||
func (s *Postgres) generateAdvisoryLockId() string {
|
||||
sum := crc32.ChecksumIEEE([]byte("ionscale_migration"))
|
||||
sum = sum * uint32(advisoryLockIDSalt)
|
||||
return fmt.Sprint(sum)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func newSqliteDB(config *config.Database, g *gorm.Config) (db, error) {
|
||||
db, err := gorm.Open(sqlite.Open(config.Url), g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Sqlite{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Sqlite struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (s *Sqlite) DB() *gorm.DB {
|
||||
return s.db
|
||||
}
|
||||
|
||||
func (s *Sqlite) Lock() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Sqlite) Unlock() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Sqlite) UnlockErr(prevErr error) error {
|
||||
return prevErr
|
||||
}
|
||||
@@ -8,19 +8,17 @@ import (
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
|
||||
ExternalID string
|
||||
LoginName string
|
||||
AuthMethodID uint64
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
ExternalID string
|
||||
LoginName string
|
||||
}
|
||||
|
||||
func (r *repository) GetOrCreateAccount(ctx context.Context, authMethodID uint64, externalID, loginName string) (*Account, bool, error) {
|
||||
func (r *repository) GetOrCreateAccount(ctx context.Context, externalID, loginName string) (*Account, bool, error) {
|
||||
account := &Account{}
|
||||
id := util.NextID()
|
||||
|
||||
tx := r.withContext(ctx).
|
||||
Where(Account{AuthMethodID: authMethodID, ExternalID: externalID}).
|
||||
Where(Account{ExternalID: externalID}).
|
||||
Attrs(Account{ID: id, LoginName: loginName}).
|
||||
FirstOrCreate(account)
|
||||
|
||||
|
||||
+290
-64
@@ -1,17 +1,36 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"inet.af/netaddr"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
const (
|
||||
AutoGroupSelf = "autogroup:self"
|
||||
AutoGroupMembers = "autogroup:members"
|
||||
AutoGroupInternet = "autogroup:internet"
|
||||
)
|
||||
|
||||
type AutoApprovers struct {
|
||||
Routes map[string][]string `json:"routes"`
|
||||
ExitNode []string `json:"exitNode"`
|
||||
}
|
||||
|
||||
type ACLPolicy struct {
|
||||
Groups map[string][]string `json:"groups,omitempty"`
|
||||
Hosts map[string]string `json:"hosts,omitempty"`
|
||||
ACLs []ACL `json:"acls"`
|
||||
Groups map[string][]string `json:"groups,omitempty"`
|
||||
Hosts map[string]string `json:"hosts,omitempty"`
|
||||
ACLs []ACL `json:"acls"`
|
||||
TagOwners map[string][]string `json:"tagowners"`
|
||||
AutoApprovers AutoApprovers `json:"autoApprovers"`
|
||||
}
|
||||
|
||||
type ACL struct {
|
||||
@@ -20,7 +39,7 @@ type ACL struct {
|
||||
Dst []string `json:"dst"`
|
||||
}
|
||||
|
||||
func defaultPolicy() ACLPolicy {
|
||||
func DefaultPolicy() ACLPolicy {
|
||||
return ACLPolicy{
|
||||
ACLs: []ACL{
|
||||
{
|
||||
@@ -32,34 +51,103 @@ func defaultPolicy() ACLPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
type aclEngine struct {
|
||||
policy *ACLPolicy
|
||||
expandedTags map[string][]string
|
||||
}
|
||||
|
||||
func IsValidPeer(policy *ACLPolicy, src *Machine, dest *Machine) bool {
|
||||
f := &aclEngine{
|
||||
policy: policy,
|
||||
func (a ACLPolicy) FindAutoApprovedIPs(routableIPs []netip.Prefix, tags []string, u *User) []netip.Prefix {
|
||||
if len(routableIPs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return f.isValidPeer(src, dest)
|
||||
}
|
||||
|
||||
func BuildFilterRules(policy *ACLPolicy, dst *Machine, peers []Machine) []tailcfg.FilterRule {
|
||||
f := &aclEngine{
|
||||
policy: policy,
|
||||
matches := func(values []string) bool {
|
||||
for _, alias := range values {
|
||||
if alias == u.Name {
|
||||
return true
|
||||
}
|
||||
|
||||
group, ok := a.Groups[alias]
|
||||
if ok {
|
||||
for _, g := range group {
|
||||
if g == u.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "tag:") {
|
||||
for _, tag := range tags {
|
||||
if alias == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return f.build(dst, peers)
|
||||
}
|
||||
|
||||
func (a *aclEngine) isValidPeer(src *Machine, dest *Machine) bool {
|
||||
for _, acl := range a.policy.ACLs {
|
||||
allDestPorts := a.expandMachineToDstPorts(dest, acl.Dst)
|
||||
if len(allDestPorts) == 0 {
|
||||
continue
|
||||
isAutoApproved := func(candidate netip.Prefix, approvedIPs []netip.Prefix) bool {
|
||||
for _, approvedIP := range approvedIPs {
|
||||
if candidate.Bits() >= approvedIP.Bits() && approvedIP.Contains(candidate.Masked().Addr()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
autoApprovedIPs := []netip.Prefix{}
|
||||
for route, autoApprovers := range a.AutoApprovers.Routes {
|
||||
candidate, err := netip.ParsePrefix(route)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, alias := range acl.Src {
|
||||
if len(a.expandMachineAlias(src, alias, true)) != 0 {
|
||||
if matches(autoApprovers) {
|
||||
autoApprovedIPs = append(autoApprovedIPs, candidate)
|
||||
}
|
||||
}
|
||||
|
||||
result := []netip.Prefix{}
|
||||
for _, c := range routableIPs {
|
||||
if c.Bits() == 0 && matches(a.AutoApprovers.ExitNode) {
|
||||
result = append(result, c)
|
||||
}
|
||||
if isAutoApproved(c, autoApprovedIPs) {
|
||||
result = append(result, c)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (a ACLPolicy) CheckTagOwners(tags []string, p *User) error {
|
||||
var result *multierror.Error
|
||||
for _, t := range tags {
|
||||
if ok := a.IsTagOwner(t, p); !ok {
|
||||
result = multierror.Append(result, fmt.Errorf("tag [%s] is invalid or not permitted", t))
|
||||
}
|
||||
}
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (a ACLPolicy) IsTagOwner(tag string, p *User) bool {
|
||||
if p.UserType == UserTypeService {
|
||||
return true
|
||||
}
|
||||
if tagOwners, ok := a.TagOwners[tag]; ok {
|
||||
return a.validateTagOwners(tagOwners, p)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a ACLPolicy) validateTagOwners(tagOwners []string, p *User) bool {
|
||||
for _, alias := range tagOwners {
|
||||
if strings.HasPrefix(alias, "group:") {
|
||||
if group, ok := a.Groups[alias]; ok {
|
||||
for _, groupMember := range group {
|
||||
if groupMember == p.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if alias == p.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -67,19 +155,40 @@ func (a *aclEngine) isValidPeer(src *Machine, dest *Machine) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *aclEngine) build(dst *Machine, peers []Machine) []tailcfg.FilterRule {
|
||||
func (a ACLPolicy) IsValidPeer(src *Machine, dest *Machine) bool {
|
||||
if !src.HasTags() && !dest.HasTags() && dest.HasUser(src.User.Name) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, acl := range a.ACLs {
|
||||
selfDestPorts, allDestPorts := a.expandMachineToDstPorts(dest, acl.Dst)
|
||||
if len(selfDestPorts) != 0 {
|
||||
for _, alias := range acl.Src {
|
||||
if len(a.expandMachineAlias(src, alias, true, &dest.User)) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(allDestPorts) != 0 {
|
||||
for _, alias := range acl.Src {
|
||||
if len(a.expandMachineAlias(src, alias, true, nil)) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (a ACLPolicy) BuildFilterRules(srcs []Machine, dst *Machine) []tailcfg.FilterRule {
|
||||
var rules []tailcfg.FilterRule
|
||||
|
||||
for _, acl := range a.policy.ACLs {
|
||||
allDestPorts := a.expandMachineToDstPorts(dst, acl.Dst)
|
||||
if len(allDestPorts) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
transform := func(src []string, destPorts []tailcfg.NetPortRange, u *User) tailcfg.FilterRule {
|
||||
var allSrcIPsSet = &StringSet{}
|
||||
for _, src := range acl.Src {
|
||||
for _, peer := range peers {
|
||||
srcIPs := a.expandMachineAlias(&peer, src, true)
|
||||
for _, alias := range src {
|
||||
for _, src := range srcs {
|
||||
srcIPs := a.expandMachineAlias(&src, alias, true, u)
|
||||
allSrcIPsSet.Add(srcIPs...)
|
||||
}
|
||||
}
|
||||
@@ -90,12 +199,20 @@ func (a *aclEngine) build(dst *Machine, peers []Machine) []tailcfg.FilterRule {
|
||||
allSrcIPs = nil
|
||||
}
|
||||
|
||||
rule := tailcfg.FilterRule{
|
||||
return tailcfg.FilterRule{
|
||||
SrcIPs: allSrcIPs,
|
||||
DstPorts: allDestPorts,
|
||||
DstPorts: destPorts,
|
||||
}
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
for _, acl := range a.ACLs {
|
||||
selfDestPorts, allDestPorts := a.expandMachineToDstPorts(dst, acl.Dst)
|
||||
if len(selfDestPorts) != 0 {
|
||||
rules = append(rules, transform(acl.Src, selfDestPorts, &dst.User))
|
||||
}
|
||||
if len(allDestPorts) != 0 {
|
||||
rules = append(rules, transform(acl.Src, allDestPorts, nil))
|
||||
}
|
||||
}
|
||||
|
||||
if len(rules) == 0 {
|
||||
@@ -105,19 +222,24 @@ func (a *aclEngine) build(dst *Machine, peers []Machine) []tailcfg.FilterRule {
|
||||
return rules
|
||||
}
|
||||
|
||||
func (a *aclEngine) expandMachineToDstPorts(m *Machine, ports []string) []tailcfg.NetPortRange {
|
||||
allDestRanges := []tailcfg.NetPortRange{}
|
||||
func (a ACLPolicy) expandMachineToDstPorts(m *Machine, ports []string) ([]tailcfg.NetPortRange, []tailcfg.NetPortRange) {
|
||||
selfDestRanges := []tailcfg.NetPortRange{}
|
||||
otherDestRanges := []tailcfg.NetPortRange{}
|
||||
for _, d := range ports {
|
||||
ranges := a.expandMachineDestToNetPortRanges(m, d)
|
||||
allDestRanges = append(allDestRanges, ranges...)
|
||||
self, ranges := a.expandMachineDestToNetPortRanges(m, d)
|
||||
if self {
|
||||
selfDestRanges = append(selfDestRanges, ranges...)
|
||||
} else {
|
||||
otherDestRanges = append(otherDestRanges, ranges...)
|
||||
}
|
||||
}
|
||||
return allDestRanges
|
||||
return selfDestRanges, otherDestRanges
|
||||
}
|
||||
|
||||
func (a *aclEngine) expandMachineDestToNetPortRanges(m *Machine, dest string) []tailcfg.NetPortRange {
|
||||
func (a ACLPolicy) expandMachineDestToNetPortRanges(m *Machine, dest string) (bool, []tailcfg.NetPortRange) {
|
||||
tokens := strings.Split(dest, ":")
|
||||
if len(tokens) < 2 || len(tokens) > 3 {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var alias string
|
||||
@@ -129,12 +251,12 @@ func (a *aclEngine) expandMachineDestToNetPortRanges(m *Machine, dest string) []
|
||||
|
||||
ports, err := a.expandValuePortToPortRange(tokens[len(tokens)-1])
|
||||
if err != nil {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ips := a.expandMachineAlias(m, alias, false)
|
||||
ips := a.expandMachineAlias(m, alias, false, nil)
|
||||
if len(ips) == 0 {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
dests := []tailcfg.NetPortRange{}
|
||||
@@ -148,22 +270,44 @@ func (a *aclEngine) expandMachineDestToNetPortRanges(m *Machine, dest string) []
|
||||
}
|
||||
}
|
||||
|
||||
return dests
|
||||
return alias == AutoGroupSelf, dests
|
||||
}
|
||||
|
||||
func (a *aclEngine) expandMachineAlias(m *Machine, alias string, src bool) []string {
|
||||
func (a ACLPolicy) expandMachineAlias(m *Machine, alias string, src bool, u *User) []string {
|
||||
if u != nil && m.HasTags() {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if u != nil && !m.HasUser(u.Name) {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if alias == "*" && u != nil {
|
||||
return m.IPs()
|
||||
}
|
||||
|
||||
if alias == "*" {
|
||||
if alias == "*" {
|
||||
return []string{"*"}
|
||||
return []string{"*"}
|
||||
}
|
||||
|
||||
if alias == AutoGroupMembers || alias == AutoGroupSelf {
|
||||
if !m.HasTags() {
|
||||
return m.IPs()
|
||||
} else {
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
if alias == AutoGroupInternet && m.IsExitNode() {
|
||||
return autogroupInternetRanges()
|
||||
}
|
||||
|
||||
if strings.Contains(alias, "@") && !m.HasTags() && m.HasUser(alias) {
|
||||
return []string{m.IPv4.String(), m.IPv6.String()}
|
||||
return m.IPs()
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "group:") && !m.HasTags() {
|
||||
users, ok := a.policy.Groups[alias]
|
||||
users, ok := a.Groups[alias]
|
||||
|
||||
if !ok {
|
||||
return []string{}
|
||||
@@ -171,33 +315,33 @@ func (a *aclEngine) expandMachineAlias(m *Machine, alias string, src bool) []str
|
||||
|
||||
for _, u := range users {
|
||||
if m.HasUser(u) {
|
||||
return []string{m.IPv4.String(), m.IPv6.String()}
|
||||
return m.IPs()
|
||||
}
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "tag:") && m.HasTag(alias[4:]) {
|
||||
return []string{m.IPv4.String(), m.IPv6.String()}
|
||||
if strings.HasPrefix(alias, "tag:") && m.HasTag(alias) {
|
||||
return m.IPs()
|
||||
}
|
||||
|
||||
if h, ok := a.policy.Hosts[alias]; ok {
|
||||
if h, ok := a.Hosts[alias]; ok {
|
||||
alias = h
|
||||
}
|
||||
|
||||
if src {
|
||||
ip, err := netaddr.ParseIP(alias)
|
||||
ip, err := netip.ParseAddr(alias)
|
||||
if err == nil && m.HasIP(ip) {
|
||||
return []string{ip.String()}
|
||||
}
|
||||
} else {
|
||||
ip, err := netaddr.ParseIP(alias)
|
||||
ip, err := netip.ParseAddr(alias)
|
||||
if err == nil && m.IsAllowedIP(ip) {
|
||||
return []string{ip.String()}
|
||||
}
|
||||
|
||||
prefix, err := netaddr.ParseIPPrefix(alias)
|
||||
prefix, err := netip.ParsePrefix(alias)
|
||||
if err == nil && m.IsAllowedIPPrefix(prefix) {
|
||||
return []string{prefix.String()}
|
||||
}
|
||||
@@ -206,7 +350,7 @@ func (a *aclEngine) expandMachineAlias(m *Machine, alias string, src bool) []str
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (a *aclEngine) expandValuePortToPortRange(s string) ([]tailcfg.PortRange, error) {
|
||||
func (a ACLPolicy) expandValuePortToPortRange(s string) ([]tailcfg.PortRange, error) {
|
||||
if s == "*" {
|
||||
return []tailcfg.PortRange{{First: 0, Last: 65535}}, nil
|
||||
}
|
||||
@@ -243,6 +387,34 @@ func (a *aclEngine) expandValuePortToPortRange(s string) ([]tailcfg.PortRange, e
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
func (i *ACLPolicy) Scan(destination interface{}) error {
|
||||
switch value := destination.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(value, i)
|
||||
default:
|
||||
return fmt.Errorf("unexpected data type %T", destination)
|
||||
}
|
||||
}
|
||||
|
||||
func (i ACLPolicy) Value() (driver.Value, error) {
|
||||
bytes, err := json.Marshal(i)
|
||||
return bytes, err
|
||||
}
|
||||
|
||||
// GormDataType gorm common data type
|
||||
func (ACLPolicy) GormDataType() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
// GormDBDataType gorm db data type
|
||||
func (ACLPolicy) GormDBDataType(db *gorm.DB, field *schema.Field) string {
|
||||
switch db.Dialector.Name() {
|
||||
case "sqlite":
|
||||
return "JSON"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type StringSet struct {
|
||||
items map[string]bool
|
||||
}
|
||||
@@ -264,5 +436,59 @@ func (s *StringSet) Items() []string {
|
||||
for i := range s.items {
|
||||
items = append(items, i)
|
||||
}
|
||||
sort.Strings(items)
|
||||
return items
|
||||
}
|
||||
|
||||
func autogroupInternetRanges() []string {
|
||||
return []string{
|
||||
"0.0.0.0/5",
|
||||
"8.0.0.0/7",
|
||||
"11.0.0.0/8",
|
||||
"12.0.0.0/6",
|
||||
"16.0.0.0/4",
|
||||
"32.0.0.0/3",
|
||||
"64.0.0.0/3",
|
||||
"96.0.0.0/6",
|
||||
"100.0.0.0/10",
|
||||
"100.128.0.0/9",
|
||||
"101.0.0.0/8",
|
||||
"102.0.0.0/7",
|
||||
"104.0.0.0/5",
|
||||
"112.0.0.0/4",
|
||||
"128.0.0.0/3",
|
||||
"160.0.0.0/5",
|
||||
"168.0.0.0/8",
|
||||
"169.0.0.0/9",
|
||||
"169.128.0.0/10",
|
||||
"169.192.0.0/11",
|
||||
"169.224.0.0/12",
|
||||
"169.240.0.0/13",
|
||||
"169.248.0.0/14",
|
||||
"169.252.0.0/15",
|
||||
"169.255.0.0/16",
|
||||
"170.0.0.0/7",
|
||||
"172.0.0.0/12",
|
||||
"172.32.0.0/11",
|
||||
"172.64.0.0/10",
|
||||
"172.128.0.0/9",
|
||||
"173.0.0.0/8",
|
||||
"174.0.0.0/7",
|
||||
"176.0.0.0/4",
|
||||
"192.0.0.0/9",
|
||||
"192.128.0.0/11",
|
||||
"192.160.0.0/13",
|
||||
"192.169.0.0/16",
|
||||
"192.170.0.0/15",
|
||||
"192.172.0.0/14",
|
||||
"192.176.0.0/12",
|
||||
"192.192.0.0/10",
|
||||
"193.0.0.0/8",
|
||||
"194.0.0.0/7",
|
||||
"196.0.0.0/6",
|
||||
"200.0.0.0/5",
|
||||
"208.0.0.0/4",
|
||||
"224.0.0.0/3",
|
||||
"2000::/3",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,654 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/addr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"tailscale.com/tailcfg"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func printRules(rules []tailcfg.FilterRule) {
|
||||
indent, err := json.MarshalIndent(rules, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(indent))
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesWildcards(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ACLs: []ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Src: []string{"*"},
|
||||
Dst: []string{"*:*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: "*",
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesWithGroups(t *testing.T) {
|
||||
p1 := createMachine("jane@example.com")
|
||||
p2 := createMachine("nick@example.com")
|
||||
p3 := createMachine("joe@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:admin": []string{"jane@example.com"},
|
||||
"group:audit": []string{"nick@example.com"},
|
||||
},
|
||||
ACLs: []ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Src: []string{"group:admin"},
|
||||
Dst: []string{"*:22"},
|
||||
},
|
||||
{
|
||||
Action: "accept",
|
||||
Src: []string{"group:audit"},
|
||||
Dst: []string{"*:8000-8080"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2, *p3}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{
|
||||
p1.IPv4.String(),
|
||||
p1.IPv6.String(),
|
||||
},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: "*",
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 22,
|
||||
Last: 22,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
SrcIPs: []string{
|
||||
p2.IPv4.String(),
|
||||
p2.IPv6.String(),
|
||||
},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: "*",
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 8000,
|
||||
Last: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesWithAutoGroupMembers(t *testing.T) {
|
||||
p1 := createMachine("jane@example.com")
|
||||
p2 := createMachine("nick@example.com")
|
||||
p3 := createMachine("joe@example.com", "tag:web")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ACLs: []ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Src: []string{"autogroup:members"},
|
||||
Dst: []string{"*:22"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2, *p3}, dst)
|
||||
|
||||
expectedSrcIPs := []string{
|
||||
p1.IPv4.String(), p1.IPv6.String(),
|
||||
p2.IPv4.String(), p2.IPv6.String(),
|
||||
}
|
||||
sort.Strings(expectedSrcIPs)
|
||||
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: expectedSrcIPs,
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: "*",
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 22,
|
||||
Last: 22,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesAutogroupSelf(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ACLs: []ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Src: []string{"*"},
|
||||
Dst: []string{"autogroup:self:*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{
|
||||
p1.IPv4.String(),
|
||||
p1.IPv6.String(),
|
||||
},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: dst.IPv4.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
},
|
||||
{
|
||||
IP: dst.IPv6.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesAutogroupSelfAndTags(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("john@example.com", "tag:web")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ACLs: []ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Src: []string{"*"},
|
||||
Dst: []string{"autogroup:self:*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{
|
||||
p1.IPv4.String(),
|
||||
p1.IPv6.String(),
|
||||
},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: dst.IPv4.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
},
|
||||
{
|
||||
IP: dst.IPv6.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesAutogroupSelfAndOtherDestinations(t *testing.T) {
|
||||
p1 := createMachine("john@example.com")
|
||||
p2 := createMachine("john@example.com", "tag:web")
|
||||
p3 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ACLs: []ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Src: []string{"*"},
|
||||
Dst: []string{"autogroup:self:22", "john@example.com:80"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2, *p3}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: p1.IPs(),
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: dst.IPv4.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 22,
|
||||
Last: 22,
|
||||
},
|
||||
},
|
||||
{
|
||||
IP: dst.IPv6.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 22,
|
||||
Last: 22,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: dst.IPv4.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 80,
|
||||
Last: 80,
|
||||
},
|
||||
},
|
||||
{
|
||||
IP: dst.IPv6.String(),
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 80,
|
||||
Last: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesAutogroupMember(t *testing.T) {
|
||||
p1 := createMachine("jane@example.com")
|
||||
p2 := createMachine("jane@example.com", "tag:web")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ACLs: []ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Src: []string{"autogroup:members"},
|
||||
Dst: []string{"*:*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{
|
||||
p1.IPv4.String(),
|
||||
p1.IPv6.String(),
|
||||
},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{
|
||||
IP: "*",
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestACLPolicy_BuildFilterRulesAutogroupInternet(t *testing.T) {
|
||||
p1 := createMachine("nick@example.com")
|
||||
p2 := createMachine("jane@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
ACLs: []ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Src: []string{"nick@example.com"},
|
||||
Dst: []string{"autogroup:internet:*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dst := createMachine("john@example.com")
|
||||
dst.AllowIPs = []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
}
|
||||
|
||||
expectedDstPorts := []tailcfg.NetPortRange{}
|
||||
for _, r := range autogroupInternetRanges() {
|
||||
expectedDstPorts = append(expectedDstPorts, tailcfg.NetPortRange{
|
||||
IP: r,
|
||||
Ports: tailcfg.PortRange{
|
||||
First: 0,
|
||||
Last: 65535,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
actualRules := policy.BuildFilterRules([]Machine{*p1, *p2}, dst)
|
||||
expectedRules := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{
|
||||
p1.IPv4.String(),
|
||||
p1.IPv6.String(),
|
||||
},
|
||||
DstPorts: expectedDstPorts,
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRules, actualRules)
|
||||
}
|
||||
|
||||
func TestWithUser(t *testing.T) {
|
||||
policy := ACLPolicy{
|
||||
ACLs: []ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Src: []string{"*"},
|
||||
Dst: []string{"john@example.com:*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
src := createMachine("john@example.com")
|
||||
assert.True(t, policy.IsValidPeer(src, createMachine("john@example.com")))
|
||||
assert.False(t, policy.IsValidPeer(src, createMachine("john@example.com", "tag:web")))
|
||||
assert.False(t, policy.IsValidPeer(src, createMachine("jane@example.com")))
|
||||
}
|
||||
|
||||
func TestWithGroup(t *testing.T) {
|
||||
policy := ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:admin": {"john@example.com"},
|
||||
},
|
||||
ACLs: []ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Src: []string{"*"},
|
||||
Dst: []string{"group:admin:*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
src := createMachine("john@example.com")
|
||||
assert.True(t, policy.IsValidPeer(src, createMachine("john@example.com")))
|
||||
assert.False(t, policy.IsValidPeer(src, createMachine("jane@example.com")))
|
||||
}
|
||||
|
||||
func TestWithTags(t *testing.T) {
|
||||
policy := ACLPolicy{
|
||||
ACLs: []ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Src: []string{"*"},
|
||||
Dst: []string{"tag:web:*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
src := createMachine("john@example.com")
|
||||
|
||||
assert.True(t, policy.IsValidPeer(src, createMachine("john@example.com", "tag:web")))
|
||||
assert.False(t, policy.IsValidPeer(src, createMachine("john@example.com", "tag:ci")))
|
||||
}
|
||||
|
||||
func TestWithHosts(t *testing.T) {
|
||||
dst1 := createMachine("john@example.com")
|
||||
dst2 := createMachine("john@example.com")
|
||||
|
||||
policy := ACLPolicy{
|
||||
Hosts: map[string]string{
|
||||
"dst1": dst1.IPv4.String(),
|
||||
},
|
||||
ACLs: []ACL{
|
||||
|
||||
{
|
||||
Action: "accept",
|
||||
Src: []string{"*"},
|
||||
Dst: []string{"dst1:*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
src := createMachine("jane@example.com")
|
||||
|
||||
assert.True(t, policy.IsValidPeer(src, dst1))
|
||||
assert.False(t, policy.IsValidPeer(src, dst2))
|
||||
}
|
||||
|
||||
func createMachine(user string, tags ...string) *Machine {
|
||||
ipv4, ipv6, err := addr.SelectIP(func(addr netip.Addr) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &Machine{
|
||||
IPv4: IP{ipv4},
|
||||
IPv6: IP{ipv6},
|
||||
User: User{
|
||||
Name: user,
|
||||
},
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLPolicy_IsTagOwner(t *testing.T) {
|
||||
policy := ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:engineers": {"jane@example.com"},
|
||||
},
|
||||
TagOwners: map[string][]string{
|
||||
"tag:web": {"john@example.com", "group:engineers"},
|
||||
}}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
tag string
|
||||
userName string
|
||||
userType UserType
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "system admin is always a valid owner",
|
||||
tag: "tag:web",
|
||||
userName: "system admin",
|
||||
userType: UserTypeService,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "system admin is always a valid owner",
|
||||
tag: "tag:unknown",
|
||||
userName: "system admin",
|
||||
userType: UserTypeService,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "direct tag owner",
|
||||
tag: "tag:web",
|
||||
userName: "john@example.com",
|
||||
userType: UserTypePerson,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "owner by group",
|
||||
tag: "tag:web",
|
||||
userName: "jane@example.com",
|
||||
userType: UserTypePerson,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "unknown owner",
|
||||
tag: "tag:web",
|
||||
userName: "nick@example.com",
|
||||
userType: UserTypePerson,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "unknown tag",
|
||||
tag: "tag:unknown",
|
||||
userName: "jane@example.com",
|
||||
userType: UserTypePerson,
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := policy.CheckTagOwners([]string{tc.tag}, &User{Name: tc.userName, UserType: tc.userType})
|
||||
if tc.expectErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLPolicy_FindAutoApprovedIPs(t *testing.T) {
|
||||
route1 := netip.MustParsePrefix("10.160.0.0/20")
|
||||
route2 := netip.MustParsePrefix("10.161.0.0/20")
|
||||
route3 := netip.MustParsePrefix("10.162.0.0/20")
|
||||
|
||||
policy := ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:admins": {"jane@example.com"},
|
||||
},
|
||||
AutoApprovers: AutoApprovers{
|
||||
Routes: map[string][]string{
|
||||
route1.String(): {"group:admins"},
|
||||
route2.String(): {"john@example.com", "tag:router"},
|
||||
},
|
||||
ExitNode: []string{"nick@example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
tag []string
|
||||
userName string
|
||||
routableIPs []netip.Prefix
|
||||
expected []netip.Prefix
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
tag: []string{},
|
||||
userName: "john@example.com",
|
||||
routableIPs: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
tag: []string{},
|
||||
userName: "john@example.com",
|
||||
routableIPs: []netip.Prefix{},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "by user",
|
||||
tag: []string{},
|
||||
userName: "john@example.com",
|
||||
routableIPs: []netip.Prefix{route1, route2, route3},
|
||||
expected: []netip.Prefix{route2},
|
||||
},
|
||||
{
|
||||
name: "partial by user",
|
||||
tag: []string{},
|
||||
userName: "john@example.com",
|
||||
routableIPs: []netip.Prefix{netip.MustParsePrefix("10.161.4.0/22")},
|
||||
expected: []netip.Prefix{netip.MustParsePrefix("10.161.4.0/22")},
|
||||
},
|
||||
{
|
||||
name: "by tag",
|
||||
tag: []string{"tag:router"},
|
||||
routableIPs: []netip.Prefix{route1, route2, route3},
|
||||
expected: []netip.Prefix{route2},
|
||||
},
|
||||
{
|
||||
name: "by group",
|
||||
userName: "jane@example.com",
|
||||
routableIPs: []netip.Prefix{route1, route2, route3},
|
||||
expected: []netip.Prefix{route1},
|
||||
},
|
||||
{
|
||||
name: "no match",
|
||||
userName: "nick@example.com",
|
||||
routableIPs: []netip.Prefix{route1, route2, route3},
|
||||
expected: []netip.Prefix{},
|
||||
},
|
||||
{
|
||||
name: "exit",
|
||||
userName: "nick@example.com",
|
||||
routableIPs: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||
expected: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||
},
|
||||
{
|
||||
name: "exit no match",
|
||||
userName: "john@example.com",
|
||||
routableIPs: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||
expected: []netip.Prefix{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualAllowedIPs := policy.FindAutoApprovedIPs(tc.routableIPs, tc.tag, &User{Name: tc.userName})
|
||||
assert.Equal(t, tc.expected, actualAllowedIPs)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CreateApiKey(tailnet *Tailnet, user *User, expiresAt *time.Time) (string, *ApiKey) {
|
||||
key := util.RandStringBytes(12)
|
||||
pwd := util.RandStringBytes(22)
|
||||
value := fmt.Sprintf("%s_%s", key, pwd)
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return value, &ApiKey{
|
||||
ID: util.NextID(),
|
||||
Key: key,
|
||||
Hash: string(hash),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
ExpiresAt: expiresAt,
|
||||
|
||||
TailnetID: tailnet.ID,
|
||||
UserID: user.ID,
|
||||
}
|
||||
}
|
||||
|
||||
type ApiKey struct {
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Key string
|
||||
Hash string
|
||||
|
||||
CreatedAt time.Time
|
||||
ExpiresAt *time.Time
|
||||
|
||||
TailnetID uint64
|
||||
Tailnet Tailnet
|
||||
|
||||
UserID uint64
|
||||
User User
|
||||
}
|
||||
|
||||
func (r *repository) SaveApiKey(ctx context.Context, key *ApiKey) error {
|
||||
tx := r.withContext(ctx).Save(key)
|
||||
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repository) LoadApiKey(ctx context.Context, key string) (*ApiKey, error) {
|
||||
split := strings.Split(key, "_")
|
||||
if len(split) != 2 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var m ApiKey
|
||||
tx := r.withContext(ctx).Preload("User").Preload("Tailnet").First(&m, "key = ?", split[0])
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(m.Hash), []byte(split[1])); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !m.ExpiresAt.IsZero() && m.ExpiresAt.Before(time.Now()) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (r *repository) DeleteApiKeysByTailnet(ctx context.Context, tailnetID uint64) error {
|
||||
tx := r.withContext(ctx).
|
||||
Where("tailnet_id = ?", tailnetID).
|
||||
Delete(&ApiKey{TailnetID: tailnetID})
|
||||
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
func (r *repository) DeleteApiKeysByUser(ctx context.Context, userID uint64) error {
|
||||
tx := r.withContext(ctx).
|
||||
Where("user_id = ?", userID).
|
||||
Delete(&ApiKey{UserID: userID})
|
||||
|
||||
return tx.Error
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
"github.com/mitchellh/pointerstructure"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AuthFilter struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Expr string
|
||||
AuthMethodID uint64
|
||||
AuthMethod AuthMethod
|
||||
TailnetID *uint64
|
||||
Tailnet *Tailnet
|
||||
}
|
||||
|
||||
type AuthFilters []AuthFilter
|
||||
|
||||
func (f *AuthFilter) Evaluate(v interface{}) (bool, error) {
|
||||
if f.Expr == "*" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
eval, err := bexpr.CreateEvaluator(f.Expr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
result, err := eval.Evaluate(v)
|
||||
if err != nil && !errors.Is(err, pointerstructure.ErrNotFound) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (fs AuthFilters) Evaluate(v interface{}) []Tailnet {
|
||||
var tailnetIDMap = make(map[uint64]bool)
|
||||
var tailnets []Tailnet
|
||||
|
||||
for _, f := range fs {
|
||||
approved, err := f.Evaluate(v)
|
||||
if err == nil && approved {
|
||||
if f.TailnetID != nil {
|
||||
_, alreadyApproved := tailnetIDMap[*f.TailnetID]
|
||||
if !alreadyApproved {
|
||||
tailnetIDMap[*f.TailnetID] = true
|
||||
tailnets = append(tailnets, *f.Tailnet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tailnets
|
||||
}
|
||||
|
||||
func (r *repository) GetAuthFilter(ctx context.Context, id uint64) (*AuthFilter, error) {
|
||||
var t AuthFilter
|
||||
tx := r.withContext(ctx).Take(&t, "id = ?", id)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveAuthFilter(ctx context.Context, m *AuthFilter) error {
|
||||
tx := r.withContext(ctx).Save(m)
|
||||
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repository) ListAuthFilters(ctx context.Context) (AuthFilters, error) {
|
||||
var filters = []AuthFilter{}
|
||||
|
||||
tx := r.withContext(ctx).
|
||||
Preload("Tailnet").
|
||||
Preload("AuthMethod").
|
||||
Find(&filters)
|
||||
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
|
||||
return filters, nil
|
||||
}
|
||||
|
||||
func (r *repository) ListAuthFiltersByAuthMethod(ctx context.Context, authMethodID uint64) (AuthFilters, error) {
|
||||
var filters = []AuthFilter{}
|
||||
|
||||
tx := r.withContext(ctx).
|
||||
Preload("Tailnet").
|
||||
Preload("AuthMethod").
|
||||
Where("auth_method_id = ?", authMethodID).Find(&filters)
|
||||
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
|
||||
return filters, nil
|
||||
}
|
||||
|
||||
func (r *repository) DeleteAuthFilter(ctx context.Context, id uint64) error {
|
||||
tx := r.withContext(ctx).Delete(&AuthFilter{ID: id})
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
func (r *repository) DeleteAuthFiltersByTailnet(ctx context.Context, tailnetID uint64) error {
|
||||
tx := r.withContext(ctx).Where("tailnet_id = ?", tailnetID).Delete(&AuthFilter{})
|
||||
return tx.Error
|
||||
}
|
||||
@@ -36,8 +36,8 @@ func CreateAuthKey(tailnet *Tailnet, user *User, ephemeral bool, tags Tags, expi
|
||||
}
|
||||
|
||||
type AuthKey struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Key string `gorm:"type:varchar(64);unique_index"`
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Key string
|
||||
Hash string
|
||||
Ephemeral bool
|
||||
Tags Tags
|
||||
@@ -93,6 +93,14 @@ func (r *repository) DeleteAuthKeysByTailnet(ctx context.Context, tailnetID uint
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
func (r *repository) DeleteAuthKeysByUser(ctx context.Context, userID uint64) error {
|
||||
tx := r.withContext(ctx).
|
||||
Where("user_id = ?", userID).
|
||||
Delete(&AuthKey{UserID: userID})
|
||||
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
func (r *repository) ListAuthKeys(ctx context.Context, tailnetID uint64) ([]AuthKey, error) {
|
||||
var authKeys = []AuthKey{}
|
||||
tx := (r.withContext(ctx).
|
||||
@@ -106,6 +114,19 @@ func (r *repository) ListAuthKeys(ctx context.Context, tailnetID uint64) ([]Auth
|
||||
return authKeys, nil
|
||||
}
|
||||
|
||||
func (r *repository) ListAuthKeysByTailnetAndUser(ctx context.Context, tailnetID, userID uint64) ([]AuthKey, error) {
|
||||
var authKeys = []AuthKey{}
|
||||
tx := (r.withContext(ctx).
|
||||
Preload("User").
|
||||
Preload("Tailnet")).
|
||||
Where("tailnet_id = ? and user_id = ?", tailnetID, userID).
|
||||
Find(&authKeys)
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
return authKeys, nil
|
||||
}
|
||||
|
||||
func (r *repository) LoadAuthKey(ctx context.Context, key string) (*AuthKey, error) {
|
||||
split := strings.Split(key, "_")
|
||||
if len(split) != 2 {
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AuthMethod struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Name string `gorm:"type:varchar(64);unique_index"`
|
||||
Type string
|
||||
Issuer string
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
func (r *repository) SaveAuthMethod(ctx context.Context, m *AuthMethod) error {
|
||||
tx := r.withContext(ctx).Save(m)
|
||||
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repository) ListAuthMethods(ctx context.Context) ([]AuthMethod, error) {
|
||||
var authMethods = []AuthMethod{}
|
||||
tx := r.withContext(ctx).Find(&authMethods)
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
return authMethods, nil
|
||||
}
|
||||
|
||||
func (r *repository) GetAuthMethod(ctx context.Context, id uint64) (*AuthMethod, error) {
|
||||
var m AuthMethod
|
||||
tx := r.withContext(ctx).First(&m, "id = ?", id)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AuthenticationRequest struct {
|
||||
Key string `gorm:"primary_key"`
|
||||
Token string
|
||||
TailnetID *uint64
|
||||
Error string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (r *repository) SaveAuthenticationRequest(ctx context.Context, session *AuthenticationRequest) error {
|
||||
tx := r.withContext(ctx).Save(session)
|
||||
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repository) GetAuthenticationRequest(ctx context.Context, key string) (*AuthenticationRequest, error) {
|
||||
var m AuthenticationRequest
|
||||
tx := r.withContext(ctx).First(&m, "key = ?", key)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (r *repository) DeleteAuthenticationRequest(ctx context.Context, key string) error {
|
||||
tx := r.withContext(ctx).Delete(&AuthenticationRequest{Key: key})
|
||||
return tx.Error
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
type DNSConfig struct {
|
||||
MagicDNS bool `json:"magic_dns"`
|
||||
OverrideLocalDNS bool `json:"override_local_dns"`
|
||||
Nameservers []string `json:"nameservers"`
|
||||
Routes map[string][]string `json:"routes"`
|
||||
}
|
||||
|
||||
func (i *DNSConfig) Scan(destination interface{}) error {
|
||||
switch value := destination.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(value, i)
|
||||
default:
|
||||
return fmt.Errorf("unexpected data type %T", destination)
|
||||
}
|
||||
}
|
||||
|
||||
func (i DNSConfig) Value() (driver.Value, error) {
|
||||
bytes, err := json.Marshal(i)
|
||||
return bytes, err
|
||||
}
|
||||
|
||||
// GormDataType gorm common data type
|
||||
func (DNSConfig) GormDataType() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
// GormDBDataType gorm db data type
|
||||
func (DNSConfig) GormDBDataType(db *gorm.DB, field *schema.Field) string {
|
||||
switch db.Dialector.Name() {
|
||||
case "sqlite":
|
||||
return "JSON"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
"github.com/mitchellh/pointerstructure"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
UserID string
|
||||
Username string
|
||||
Email string
|
||||
Attr map[string]interface{}
|
||||
}
|
||||
|
||||
type IAMPolicy struct {
|
||||
Subs []string `json:"subs,omitempty"`
|
||||
Emails []string `json:"emails,omitempty"`
|
||||
Filters []string `json:"filters,omitempty"`
|
||||
Roles map[string]UserRole `json:"roles,omitempty"`
|
||||
}
|
||||
|
||||
func (i *IAMPolicy) GetRole(user User) UserRole {
|
||||
if val, ok := i.Roles[user.Name]; ok {
|
||||
return val
|
||||
}
|
||||
return UserRoleMember
|
||||
}
|
||||
|
||||
func (i *IAMPolicy) EvaluatePolicy(identity *Identity) (bool, error) {
|
||||
for _, sub := range i.Subs {
|
||||
if identity.UserID == sub {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, email := range i.Emails {
|
||||
if identity.Email == email {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range i.Filters {
|
||||
if f == "*" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
evaluator, err := bexpr.CreateEvaluator(f)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
result, err := evaluator.Evaluate(identity.Attr)
|
||||
if err != nil && !errors.Is(err, pointerstructure.ErrNotFound) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if result {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (i *IAMPolicy) Scan(destination interface{}) error {
|
||||
switch value := destination.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(value, i)
|
||||
default:
|
||||
return fmt.Errorf("unexpected data type %T", destination)
|
||||
}
|
||||
}
|
||||
|
||||
func (i IAMPolicy) Value() (driver.Value, error) {
|
||||
bytes, err := json.Marshal(i)
|
||||
return bytes, err
|
||||
}
|
||||
|
||||
// GormDataType gorm common data type
|
||||
func (IAMPolicy) GormDataType() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
// GormDBDataType gorm db data type
|
||||
func (IAMPolicy) GormDBDataType(db *gorm.DB, field *schema.Field) string {
|
||||
switch db.Dialector.Name() {
|
||||
case "sqlite":
|
||||
return "JSON"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
+160
-46
@@ -8,31 +8,33 @@ import (
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
"inet.af/netaddr"
|
||||
"net/netip"
|
||||
"tailscale.com/tailcfg"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Machine struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Name string
|
||||
NameIdx uint64
|
||||
MachineKey string
|
||||
NodeKey string
|
||||
DiscoKey string
|
||||
Ephemeral bool
|
||||
RegisteredTags Tags
|
||||
Tags Tags
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Name string
|
||||
NameIdx uint64
|
||||
MachineKey string
|
||||
NodeKey string
|
||||
DiscoKey string
|
||||
Ephemeral bool
|
||||
RegisteredTags Tags
|
||||
Tags Tags
|
||||
KeyExpiryDisabled bool
|
||||
|
||||
HostInfo HostInfo
|
||||
Endpoints Endpoints
|
||||
AllowIPs AllowIPs
|
||||
HostInfo HostInfo
|
||||
Endpoints Endpoints
|
||||
AllowIPs AllowIPs
|
||||
AutoAllowIPs AllowIPs
|
||||
|
||||
IPv4 IP
|
||||
IPv6 IP
|
||||
|
||||
CreatedAt time.Time
|
||||
ExpiresAt *time.Time
|
||||
ExpiresAt time.Time
|
||||
LastSeen *time.Time
|
||||
|
||||
UserID uint64
|
||||
@@ -44,12 +46,16 @@ type Machine struct {
|
||||
|
||||
type Machines []Machine
|
||||
|
||||
func (m *Machine) IsExpired() bool {
|
||||
return m.ExpiresAt != nil && !m.ExpiresAt.IsZero() && m.ExpiresAt.Before(time.Now())
|
||||
func (m *Machine) IPs() []string {
|
||||
return []string{m.IPv4.String(), m.IPv6.String()}
|
||||
}
|
||||
|
||||
func (m *Machine) HasIP(v netaddr.IP) bool {
|
||||
return v.Compare(*m.IPv4.IP) == 0 || v.Compare(*m.IPv6.IP) == 0
|
||||
func (m *Machine) IsExpired() bool {
|
||||
return !m.KeyExpiryDisabled && !m.ExpiresAt.IsZero() && m.ExpiresAt.Before(time.Now())
|
||||
}
|
||||
|
||||
func (m *Machine) HasIP(v netip.Addr) bool {
|
||||
return v.Compare(*m.IPv4.Addr) == 0 || v.Compare(*m.IPv6.Addr) == 0
|
||||
}
|
||||
|
||||
func (m *Machine) HasTag(tag string) bool {
|
||||
@@ -69,7 +75,55 @@ func (m *Machine) HasTags() bool {
|
||||
return len(m.Tags) != 0
|
||||
}
|
||||
|
||||
func (m *Machine) IsAllowedIP(i netaddr.IP) bool {
|
||||
func (m *Machine) IsAdvertisedExitNode() bool {
|
||||
for _, r := range m.HostInfo.RoutableIPs {
|
||||
if r.Bits() == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Machine) IsAllowedExitNode() bool {
|
||||
for _, r := range m.AllowIPs {
|
||||
if r.Bits() == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, r := range m.AutoAllowIPs {
|
||||
if r.Bits() == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Machine) AdvertisedPrefixes() []string {
|
||||
result := []string{}
|
||||
for _, r := range m.HostInfo.RoutableIPs {
|
||||
if r.Bits() != 0 {
|
||||
result = append(result, r.String())
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *Machine) AllowedPrefixes() []string {
|
||||
result := StringSet{}
|
||||
for _, r := range m.AllowIPs {
|
||||
if r.Bits() != 0 {
|
||||
result.Add(r.String())
|
||||
}
|
||||
}
|
||||
for _, r := range m.AutoAllowIPs {
|
||||
if r.Bits() != 0 {
|
||||
result.Add(r.String())
|
||||
}
|
||||
}
|
||||
return result.Items()
|
||||
}
|
||||
|
||||
func (m *Machine) IsAllowedIP(i netip.Addr) bool {
|
||||
if m.HasIP(i) {
|
||||
return true
|
||||
}
|
||||
@@ -78,26 +132,50 @@ func (m *Machine) IsAllowedIP(i netaddr.IP) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, t := range m.AutoAllowIPs {
|
||||
if t.Contains(i) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Machine) IsAllowedIPPrefix(i netaddr.IPPrefix) bool {
|
||||
func (m *Machine) IsAllowedIPPrefix(i netip.Prefix) bool {
|
||||
for _, t := range m.AllowIPs {
|
||||
if t.Overlaps(i) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, t := range m.AutoAllowIPs {
|
||||
if t.Overlaps(i) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Machine) IsExitNode() bool {
|
||||
for _, t := range m.AllowIPs {
|
||||
if t.Bits() == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, t := range m.AutoAllowIPs {
|
||||
if t.Bits() == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type IP struct {
|
||||
*netaddr.IP
|
||||
*netip.Addr
|
||||
}
|
||||
|
||||
func (i *IP) Scan(destination interface{}) error {
|
||||
switch value := destination.(type) {
|
||||
case string:
|
||||
ip, err := netaddr.ParseIP(value)
|
||||
ip, err := netip.ParseAddr(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -109,13 +187,62 @@ func (i *IP) Scan(destination interface{}) error {
|
||||
}
|
||||
|
||||
func (i IP) Value() (driver.Value, error) {
|
||||
if i.IP == nil {
|
||||
if i.Addr == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return i.String(), nil
|
||||
}
|
||||
|
||||
type AllowIPs []netaddr.IPPrefix
|
||||
func (IP) GormDBDataType(db *gorm.DB, field *schema.Field) string {
|
||||
switch db.Dialector.Name() {
|
||||
case "postgres":
|
||||
return "TEXT"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type AllowIPs []netip.Prefix
|
||||
|
||||
type AllowIPsSet struct {
|
||||
items map[netip.Prefix]bool
|
||||
}
|
||||
|
||||
func NewAllowIPsSet(t AllowIPs) *AllowIPsSet {
|
||||
s := &AllowIPsSet{}
|
||||
return s.Add(t...)
|
||||
}
|
||||
|
||||
func (s *AllowIPsSet) Add(t ...netip.Prefix) *AllowIPsSet {
|
||||
if s.items == nil {
|
||||
s.items = make(map[netip.Prefix]bool)
|
||||
}
|
||||
|
||||
for _, v := range t {
|
||||
s.items[v] = true
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *AllowIPsSet) Remove(t ...netip.Prefix) *AllowIPsSet {
|
||||
if s.items == nil {
|
||||
return s
|
||||
}
|
||||
|
||||
for _, v := range t {
|
||||
delete(s.items, v)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *AllowIPsSet) Items() []netip.Prefix {
|
||||
items := []netip.Prefix{}
|
||||
for i := range s.items {
|
||||
items = append(items, i)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (hi *AllowIPs) Scan(destination interface{}) error {
|
||||
switch value := destination.(type) {
|
||||
@@ -313,6 +440,11 @@ func (r *repository) DeleteMachineByTailnet(ctx context.Context, tailnetID uint6
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
func (r *repository) DeleteMachineByUser(ctx context.Context, userID uint64) error {
|
||||
tx := r.withContext(ctx).Model(&Machine{}).Where("user_id = ?", userID).Delete(&Machine{})
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
func (r *repository) ListMachineByTailnet(ctx context.Context, tailnetID uint64) (Machines, error) {
|
||||
var machines = []Machine{}
|
||||
|
||||
@@ -363,7 +495,10 @@ func (r *repository) ListInactiveEphemeralMachines(ctx context.Context, t time.T
|
||||
|
||||
func (r *repository) SetMachineLastSeen(ctx context.Context, machineID uint64) error {
|
||||
now := time.Now().UTC()
|
||||
tx := r.withContext(ctx).Model(Machine{}).Where("id = ?", machineID).Updates(map[string]interface{}{"last_seen": &now})
|
||||
tx := r.withContext(ctx).
|
||||
Model(Machine{}).
|
||||
Where("id = ?", machineID).
|
||||
Updates(map[string]interface{}{"last_seen": &now})
|
||||
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
@@ -371,24 +506,3 @@ func (r *repository) SetMachineLastSeen(ctx context.Context, machineID uint64) e
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repository) ExpireMachineByAuthMethod(ctx context.Context, authMethodID uint64) (int64, error) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
subQuery := r.withContext(ctx).
|
||||
Select("machines.id").
|
||||
Table("machines").
|
||||
Joins("JOIN users u on u.id = machines.user_id JOIN accounts a on a.id = u.account_id").
|
||||
Where("a.auth_method_id = ?", authMethodID)
|
||||
|
||||
tx := r.withContext(ctx).
|
||||
Table("machines").
|
||||
Where("tags = '' AND (expires_at is null or expires_at > ?) AND id in (?)", &now, subQuery).
|
||||
Updates(map[string]interface{}{"expires_at": &now})
|
||||
|
||||
if tx.Error != nil {
|
||||
return 0, tx.Error
|
||||
}
|
||||
|
||||
return tx.RowsAffected, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package domain
|
||||
|
||||
type Principal struct {
|
||||
SystemRole SystemRole
|
||||
User *User
|
||||
UserRole UserRole
|
||||
}
|
||||
|
||||
func (p Principal) IsSystemAdmin() bool {
|
||||
return p.SystemRole.IsAdmin()
|
||||
}
|
||||
|
||||
func (p Principal) IsTailnetAdmin(tailnetID uint64) bool {
|
||||
return p.User.TailnetID == tailnetID && p.UserRole.IsAdmin()
|
||||
}
|
||||
|
||||
func (p Principal) IsTailnetMember(tailnetID uint64) bool {
|
||||
return p.User.TailnetID == tailnetID
|
||||
}
|
||||
|
||||
func (p Principal) UserMatches(userID uint64) bool {
|
||||
return p.User.ID == userID
|
||||
}
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
)
|
||||
|
||||
type RegistrationRequest struct {
|
||||
MachineKey string `gorm:"primary_key;autoIncrement:false"`
|
||||
Key string `gorm:"type:varchar(64);unique_index"`
|
||||
MachineKey string `gorm:"primary_key"`
|
||||
Key string
|
||||
Data RegistrationRequestData
|
||||
CreatedAt time.Time
|
||||
Authenticated bool
|
||||
|
||||
@@ -2,54 +2,52 @@ package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"sync"
|
||||
"tailscale.com/tailcfg"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetControlKeys(ctx context.Context) (*ControlKeys, error)
|
||||
SetControlKeys(ctx context.Context, v *ControlKeys) error
|
||||
SetControlKeys(ctx context.Context, keys *ControlKeys) error
|
||||
|
||||
GetDERPMap(ctx context.Context) (*tailcfg.DERPMap, error)
|
||||
SetDERPMap(ctx context.Context, v *tailcfg.DERPMap) error
|
||||
|
||||
SaveAuthMethod(ctx context.Context, m *AuthMethod) error
|
||||
ListAuthMethods(ctx context.Context) ([]AuthMethod, error)
|
||||
GetAuthMethod(ctx context.Context, id uint64) (*AuthMethod, error)
|
||||
|
||||
GetAuthFilter(ctx context.Context, id uint64) (*AuthFilter, error)
|
||||
SaveAuthFilter(ctx context.Context, m *AuthFilter) error
|
||||
ListAuthFilters(ctx context.Context) (AuthFilters, error)
|
||||
ListAuthFiltersByAuthMethod(ctx context.Context, authMethodID uint64) (AuthFilters, error)
|
||||
DeleteAuthFilter(ctx context.Context, id uint64) error
|
||||
DeleteAuthFiltersByTailnet(ctx context.Context, tailnetID uint64) error
|
||||
|
||||
GetAccount(ctx context.Context, accountID uint64) (*Account, error)
|
||||
GetOrCreateAccount(ctx context.Context, authMethodID uint64, externalID, loginName string) (*Account, bool, error)
|
||||
GetOrCreateAccount(ctx context.Context, externalID, loginName string) (*Account, bool, error)
|
||||
|
||||
GetOrCreateTailnet(ctx context.Context, name string) (*Tailnet, bool, error)
|
||||
SaveTailnet(ctx context.Context, tailnet *Tailnet) error
|
||||
GetOrCreateTailnet(ctx context.Context, name string, iamPolicy IAMPolicy) (*Tailnet, bool, error)
|
||||
GetTailnet(ctx context.Context, id uint64) (*Tailnet, error)
|
||||
ListTailnets(ctx context.Context) ([]Tailnet, error)
|
||||
DeleteTailnet(ctx context.Context, id uint64) error
|
||||
|
||||
GetDNSConfig(ctx context.Context, tailnetID uint64) (*DNSConfig, error)
|
||||
SetDNSConfig(ctx context.Context, tailnetID uint64, config *DNSConfig) error
|
||||
DeleteDNSConfig(ctx context.Context, tailnetID uint64) error
|
||||
GetACLPolicy(ctx context.Context, tailnetID uint64) (*ACLPolicy, error)
|
||||
SetACLPolicy(ctx context.Context, tailnetID uint64, policy *ACLPolicy) error
|
||||
DeleteACLPolicy(ctx context.Context, tailnetID uint64) error
|
||||
SaveSystemApiKey(ctx context.Context, key *SystemApiKey) error
|
||||
LoadSystemApiKey(ctx context.Context, key string) (*SystemApiKey, error)
|
||||
|
||||
SaveApiKey(ctx context.Context, key *ApiKey) error
|
||||
LoadApiKey(ctx context.Context, key string) (*ApiKey, error)
|
||||
DeleteApiKeysByTailnet(ctx context.Context, tailnetID uint64) error
|
||||
DeleteApiKeysByUser(ctx context.Context, userID uint64) error
|
||||
|
||||
GetAuthKey(ctx context.Context, id uint64) (*AuthKey, error)
|
||||
SaveAuthKey(ctx context.Context, key *AuthKey) error
|
||||
DeleteAuthKey(ctx context.Context, id uint64) (bool, error)
|
||||
DeleteAuthKeysByTailnet(ctx context.Context, tailnetID uint64) error
|
||||
DeleteAuthKeysByUser(ctx context.Context, userID uint64) error
|
||||
ListAuthKeys(ctx context.Context, tailnetID uint64) ([]AuthKey, error)
|
||||
ListAuthKeysByTailnetAndUser(ctx context.Context, tailnetID, userID uint64) ([]AuthKey, error)
|
||||
LoadAuthKey(ctx context.Context, key string) (*AuthKey, error)
|
||||
|
||||
GetOrCreateServiceUser(ctx context.Context, tailnet *Tailnet) (*User, bool, error)
|
||||
ListUsers(ctx context.Context, tailnetID uint64) (Users, error)
|
||||
GetOrCreateUserWithAccount(ctx context.Context, tailnet *Tailnet, account *Account) (*User, bool, error)
|
||||
GetUser(ctx context.Context, userID uint64) (*User, error)
|
||||
DeleteUser(ctx context.Context, userID uint64) error
|
||||
ListUsers(ctx context.Context, tailnetID uint64) (Users, error)
|
||||
DeleteUsersByTailnet(ctx context.Context, tailnetID uint64) error
|
||||
|
||||
SaveMachine(ctx context.Context, m *Machine) error
|
||||
@@ -62,26 +60,32 @@ type Repository interface {
|
||||
ListMachineByTailnet(ctx context.Context, tailnetID uint64) (Machines, error)
|
||||
CountMachineByTailnet(ctx context.Context, tailnetID uint64) (int64, error)
|
||||
DeleteMachineByTailnet(ctx context.Context, tailnetID uint64) error
|
||||
DeleteMachineByUser(ctx context.Context, userID uint64) error
|
||||
ListMachinePeers(ctx context.Context, tailnetID uint64, key string) (Machines, error)
|
||||
ListInactiveEphemeralMachines(ctx context.Context, checkpoint time.Time) (Machines, error)
|
||||
SetMachineLastSeen(ctx context.Context, machineID uint64) error
|
||||
ExpireMachineByAuthMethod(ctx context.Context, authMethodID uint64) (int64, error)
|
||||
|
||||
SaveRegistrationRequest(ctx context.Context, request *RegistrationRequest) error
|
||||
GetRegistrationRequestByKey(ctx context.Context, key string) (*RegistrationRequest, error)
|
||||
GetRegistrationRequestByMachineKey(ctx context.Context, key string) (*RegistrationRequest, error)
|
||||
|
||||
SaveAuthenticationRequest(ctx context.Context, session *AuthenticationRequest) error
|
||||
GetAuthenticationRequest(ctx context.Context, key string) (*AuthenticationRequest, error)
|
||||
DeleteAuthenticationRequest(ctx context.Context, key string) error
|
||||
|
||||
Transaction(func(rp Repository) error) error
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) Repository {
|
||||
return &repository{
|
||||
db: db,
|
||||
db: db,
|
||||
defaultDERPMap: &derpMapCache{},
|
||||
}
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
defaultDERPMap *derpMapCache
|
||||
}
|
||||
|
||||
func (r *repository) withContext(ctx context.Context) *gorm.DB {
|
||||
@@ -93,3 +97,41 @@ func (r *repository) Transaction(action func(Repository) error) error {
|
||||
return action(NewRepository(tx))
|
||||
})
|
||||
}
|
||||
|
||||
type derpMapCache struct {
|
||||
sync.RWMutex
|
||||
value *tailcfg.DERPMap
|
||||
}
|
||||
|
||||
func (d *derpMapCache) Get() (*tailcfg.DERPMap, error) {
|
||||
d.RLock()
|
||||
|
||||
if d.value != nil {
|
||||
d.RUnlock()
|
||||
return d.value, nil
|
||||
}
|
||||
d.RUnlock()
|
||||
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
|
||||
getJson := func(url string, target interface{}) error {
|
||||
c := http.Client{Timeout: 5 * time.Second}
|
||||
r, err := c.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
return json.NewDecoder(r.Body).Decode(target)
|
||||
}
|
||||
|
||||
m := &tailcfg.DERPMap{}
|
||||
if err := getJson("https://controlplane.tailscale.com/derpmap/default", m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.value = m
|
||||
|
||||
return d.value, nil
|
||||
}
|
||||
|
||||
@@ -6,28 +6,27 @@ import (
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
tkey "tailscale.com/types/key"
|
||||
)
|
||||
|
||||
type configKey string
|
||||
|
||||
const (
|
||||
controlKeysConfigKey configKey = "control_keys"
|
||||
derpMapConfigKey configKey = "derp_map"
|
||||
controlKeysConfigKey configKey = "control_keys"
|
||||
)
|
||||
|
||||
type ControlKeys struct {
|
||||
ControlKey key.MachinePrivate
|
||||
LegacyControlKey key.MachinePrivate
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Key configKey `gorm:"primary_key"`
|
||||
Value []byte
|
||||
}
|
||||
|
||||
func (r *repository) GetControlKeys(ctx context.Context) (*ControlKeys, error) {
|
||||
type ControlKeys struct {
|
||||
ControlKey tkey.MachinePrivate
|
||||
LegacyControlKey tkey.MachinePrivate
|
||||
}
|
||||
|
||||
func (r *repository) GetControlKeys(ctx context.Context) (*ControlKeys, error) {
|
||||
var m ControlKeys
|
||||
err := r.getServerConfig(ctx, controlKeysConfigKey, &m)
|
||||
|
||||
@@ -52,7 +51,7 @@ func (r *repository) GetDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
|
||||
err := r.getServerConfig(ctx, derpMapConfigKey, &m)
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
return r.defaultDERPMap.Get()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CreateSystemApiKey(account *Account, expiresAt *time.Time) (string, *SystemApiKey) {
|
||||
key := util.RandStringBytes(12)
|
||||
pwd := util.RandStringBytes(22)
|
||||
value := fmt.Sprintf("sk_%s_%s", key, pwd)
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return value, &SystemApiKey{
|
||||
ID: util.NextID(),
|
||||
Key: key,
|
||||
Hash: string(hash),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
ExpiresAt: expiresAt,
|
||||
|
||||
AccountID: account.ID,
|
||||
}
|
||||
}
|
||||
|
||||
type SystemApiKey struct {
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Key string
|
||||
Hash string
|
||||
|
||||
CreatedAt time.Time
|
||||
ExpiresAt *time.Time
|
||||
|
||||
AccountID uint64
|
||||
Account Account
|
||||
}
|
||||
|
||||
func (r *repository) SaveSystemApiKey(ctx context.Context, key *SystemApiKey) error {
|
||||
tx := r.withContext(ctx).Save(key)
|
||||
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repository) LoadSystemApiKey(ctx context.Context, token string) (*SystemApiKey, error) {
|
||||
split := strings.Split(token, "_")
|
||||
if len(split) != 3 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
prefix := split[0]
|
||||
key := split[1]
|
||||
value := split[2]
|
||||
|
||||
if prefix != "sk" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var m SystemApiKey
|
||||
tx := r.withContext(ctx).Preload("Account").First(&m, "key = ?", key)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(m.Hash), []byte(value)); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !m.ExpiresAt.IsZero() && m.ExpiresAt.Before(time.Now()) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
+16
-14
@@ -3,7 +3,9 @@ package domain
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"strings"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
type Tags []string
|
||||
@@ -31,21 +33,21 @@ func (i Tags) Value() (driver.Value, error) {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func SanitizeTags(input []string) Tags {
|
||||
keys := make(map[string]bool)
|
||||
var tags []string
|
||||
for _, v := range input {
|
||||
var entry string
|
||||
if strings.HasPrefix(v, "tag:") {
|
||||
entry = v[4:]
|
||||
} else {
|
||||
entry = v
|
||||
}
|
||||
func CheckTag(tag string) error {
|
||||
return tailcfg.CheckTag(tag)
|
||||
}
|
||||
|
||||
if _, value := keys[entry]; !value {
|
||||
keys[entry] = true
|
||||
tags = append(tags, entry)
|
||||
func CheckTags(tags []string) error {
|
||||
var result *multierror.Error
|
||||
for _, t := range tags {
|
||||
if err := CheckTag(t); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
}
|
||||
return tags
|
||||
return result.ErrorOrNil()
|
||||
}
|
||||
|
||||
func SanitizeTags(input []string) Tags {
|
||||
s := StringSet{}
|
||||
return s.Add(input...).Items()
|
||||
}
|
||||
|
||||
@@ -5,18 +5,54 @@ import (
|
||||
"errors"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"gorm.io/gorm"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
type Tailnet struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Name string `gorm:"type:varchar(64);unique_index"`
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Name string
|
||||
DNSConfig DNSConfig
|
||||
IAMPolicy IAMPolicy
|
||||
ACLPolicy ACLPolicy
|
||||
}
|
||||
|
||||
func (r *repository) GetOrCreateTailnet(ctx context.Context, name string) (*Tailnet, bool, error) {
|
||||
func SanitizeTailnetName(name string) string {
|
||||
name = strings.ToLower(name)
|
||||
|
||||
a, err := mail.ParseAddress(name)
|
||||
if err == nil && a.Address == name {
|
||||
s := strings.Split(name, "@")
|
||||
return strings.Join([]string{dnsname.SanitizeLabel(s[0]), s[1]}, ".")
|
||||
}
|
||||
|
||||
labels := strings.Split(name, ".")
|
||||
for i, s := range labels {
|
||||
labels[i] = dnsname.SanitizeLabel(s)
|
||||
}
|
||||
|
||||
return strings.Join(labels, ".")
|
||||
}
|
||||
|
||||
func (r *repository) SaveTailnet(ctx context.Context, tailnet *Tailnet) error {
|
||||
tx := r.withContext(ctx).Save(tailnet)
|
||||
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repository) GetOrCreateTailnet(ctx context.Context, name string, iamPolicy IAMPolicy) (*Tailnet, bool, error) {
|
||||
tailnet := &Tailnet{}
|
||||
id := util.NextID()
|
||||
|
||||
tx := r.withContext(ctx).Where(Tailnet{Name: name}).Attrs(Tailnet{ID: id}).FirstOrCreate(tailnet)
|
||||
tx := r.withContext(ctx).
|
||||
Where(Tailnet{Name: name}).
|
||||
Attrs(Tailnet{ID: id, ACLPolicy: DefaultPolicy(), IAMPolicy: iamPolicy}).
|
||||
FirstOrCreate(tailnet)
|
||||
|
||||
if tx.Error != nil {
|
||||
return nil, false, tx.Error
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TailnetConfig struct {
|
||||
Key string `gorm:"primary_key"`
|
||||
TailnetID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Value []byte
|
||||
}
|
||||
|
||||
type DNSConfig struct {
|
||||
MagicDNS bool
|
||||
OverrideLocalDNS bool
|
||||
Nameservers []string
|
||||
Routes map[string][]string
|
||||
}
|
||||
|
||||
func (r *repository) GetDNSConfig(ctx context.Context, tailnetID uint64) (*DNSConfig, error) {
|
||||
var m DNSConfig
|
||||
err := r.getConfig(ctx, "dns_config", tailnetID, &m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (r *repository) SetDNSConfig(ctx context.Context, tailnetID uint64, config *DNSConfig) error {
|
||||
return r.setConfig(ctx, "dns_config", tailnetID, config)
|
||||
}
|
||||
|
||||
func (r *repository) DeleteDNSConfig(ctx context.Context, tailnetID uint64) error {
|
||||
return r.deleteConfig(ctx, "dns_config", tailnetID)
|
||||
}
|
||||
|
||||
func (r *repository) SetACLPolicy(ctx context.Context, tailnetID uint64, policy *ACLPolicy) error {
|
||||
if err := r.setConfig(ctx, "acl_policy", tailnetID, policy); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repository) GetACLPolicy(ctx context.Context, tailnetID uint64) (*ACLPolicy, error) {
|
||||
var p = defaultPolicy()
|
||||
err := r.getConfig(ctx, "acl_policy", tailnetID, &p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (r *repository) DeleteACLPolicy(ctx context.Context, tailnetID uint64) error {
|
||||
return r.deleteConfig(ctx, "acl_policy", tailnetID)
|
||||
}
|
||||
|
||||
func (r *repository) getConfig(ctx context.Context, s string, tailnetID uint64, v interface{}) error {
|
||||
var m TailnetConfig
|
||||
tx := r.withContext(ctx).Take(&m, "key = ? AND tailnet_id = ?", s, tailnetID)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
err := json.Unmarshal(m.Value, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repository) setConfig(ctx context.Context, s string, tailnetID uint64, v interface{}) error {
|
||||
marshal, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := &TailnetConfig{
|
||||
Key: s,
|
||||
Value: marshal,
|
||||
TailnetID: tailnetID,
|
||||
}
|
||||
tx := r.withContext(ctx).Save(c)
|
||||
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
func (r *repository) deleteConfig(ctx context.Context, s string, tailnetID uint64) error {
|
||||
c := &TailnetConfig{
|
||||
Key: s,
|
||||
TailnetID: tailnetID,
|
||||
}
|
||||
tx := r.withContext(ctx).Delete(c)
|
||||
|
||||
return tx.Error
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSanitizeTailnetName(t *testing.T) {
|
||||
assert.Equal(t, "john.example.com", SanitizeTailnetName("john@example.com"))
|
||||
assert.Equal(t, "john.example.com", SanitizeTailnetName("john@examPle.Com"))
|
||||
assert.Equal(t, "john-doe.example.com", SanitizeTailnetName("john.doe@example.com"))
|
||||
assert.Equal(t, "johns-network", SanitizeTailnetName("John's Network"))
|
||||
assert.Equal(t, "example.com", SanitizeTailnetName("example.com"))
|
||||
assert.Equal(t, "johns-example.com", SanitizeTailnetName("John's example.com"))
|
||||
}
|
||||
+57
-14
@@ -2,24 +2,47 @@ package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TailnetRole string
|
||||
type SystemRole string
|
||||
|
||||
const (
|
||||
TailnetRoleService TailnetRole = "service"
|
||||
TailnetRoleMember TailnetRole = "member"
|
||||
SystemRoleNone SystemRole = ""
|
||||
SystemRoleAdmin SystemRole = "admin"
|
||||
)
|
||||
|
||||
func (s SystemRole) IsAdmin() bool {
|
||||
return s == SystemRoleAdmin
|
||||
}
|
||||
|
||||
type UserType string
|
||||
|
||||
const (
|
||||
UserTypeService UserType = "service"
|
||||
UserTypePerson UserType = "person"
|
||||
)
|
||||
|
||||
type UserRole string
|
||||
|
||||
const (
|
||||
UserRoleNone UserRole = ""
|
||||
UserRoleMember UserRole = "member"
|
||||
UserRoleAdmin UserRole = "admin"
|
||||
)
|
||||
|
||||
func (s UserRole) IsAdmin() bool {
|
||||
return s == UserRoleAdmin
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uint64 `gorm:"primary_key;autoIncrement:false"`
|
||||
Name string
|
||||
|
||||
TailnetRole TailnetRole
|
||||
TailnetID uint64
|
||||
Tailnet Tailnet
|
||||
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
Name string
|
||||
UserType UserType
|
||||
TailnetID uint64
|
||||
Tailnet Tailnet
|
||||
AccountID *uint64
|
||||
Account *Account
|
||||
}
|
||||
@@ -30,8 +53,8 @@ func (r *repository) GetOrCreateServiceUser(ctx context.Context, tailnet *Tailne
|
||||
user := &User{}
|
||||
id := util.NextID()
|
||||
|
||||
query := User{Name: tailnet.Name, TailnetID: tailnet.ID, TailnetRole: TailnetRoleService}
|
||||
attrs := User{ID: id, Name: tailnet.Name, TailnetID: tailnet.ID, TailnetRole: TailnetRoleService}
|
||||
query := User{Name: tailnet.Name, TailnetID: tailnet.ID, UserType: UserTypeService}
|
||||
attrs := User{ID: id, Name: tailnet.Name, TailnetID: tailnet.ID, UserType: UserTypeService}
|
||||
|
||||
tx := r.withContext(ctx).Where(query).Attrs(attrs).FirstOrCreate(user)
|
||||
|
||||
@@ -45,7 +68,7 @@ func (r *repository) GetOrCreateServiceUser(ctx context.Context, tailnet *Tailne
|
||||
func (r *repository) ListUsers(ctx context.Context, tailnetID uint64) (Users, error) {
|
||||
var users = []User{}
|
||||
|
||||
tx := r.withContext(ctx).Where("tailnet_id = ?", tailnetID).Find(&users)
|
||||
tx := r.withContext(ctx).Where("tailnet_id = ? AND user_type = ?", tailnetID, UserTypePerson).Find(&users)
|
||||
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
@@ -64,7 +87,7 @@ func (r *repository) GetOrCreateUserWithAccount(ctx context.Context, tailnet *Ta
|
||||
id := util.NextID()
|
||||
|
||||
query := User{AccountID: &account.ID, TailnetID: tailnet.ID}
|
||||
attrs := User{ID: id, Name: account.LoginName, TailnetID: tailnet.ID, AccountID: &account.ID, TailnetRole: TailnetRoleMember}
|
||||
attrs := User{ID: id, Name: account.LoginName, TailnetID: tailnet.ID, AccountID: &account.ID, UserType: UserTypePerson}
|
||||
|
||||
tx := r.withContext(ctx).Where(query).Attrs(attrs).FirstOrCreate(user)
|
||||
|
||||
@@ -74,3 +97,23 @@ func (r *repository) GetOrCreateUserWithAccount(ctx context.Context, tailnet *Ta
|
||||
|
||||
return user, user.ID == id, nil
|
||||
}
|
||||
|
||||
func (r *repository) GetUser(ctx context.Context, userID uint64) (*User, error) {
|
||||
var m User
|
||||
tx := r.withContext(ctx).Preload("Tailnet").Preload("Account").First(&m, "id = ? and user_type = ?", userID, UserTypePerson)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (r *repository) DeleteUser(ctx context.Context, userID uint64) error {
|
||||
tx := r.withContext(ctx).Delete(&User{ID: userID})
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
+245
-114
@@ -5,9 +5,9 @@ import (
|
||||
"encoding/json"
|
||||
"github.com/jsiebens/ionscale/internal/addr"
|
||||
"github.com/jsiebens/ionscale/internal/provider"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/mr-tron/base58"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"tailscale.com/tailcfg"
|
||||
"time"
|
||||
|
||||
@@ -15,37 +15,67 @@ import (
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
func NewAuthenticationHandlers(
|
||||
config *config.Config,
|
||||
authProvider provider.AuthProvider,
|
||||
systemIAMPolicy *domain.IAMPolicy,
|
||||
repository domain.Repository) *AuthenticationHandlers {
|
||||
|
||||
return &AuthenticationHandlers{
|
||||
config: config,
|
||||
repository: repository,
|
||||
pendingOAuthUsers: cache.New(5*time.Minute, 10*time.Minute),
|
||||
config: config,
|
||||
authProvider: authProvider,
|
||||
repository: repository,
|
||||
systemIAMPolicy: systemIAMPolicy,
|
||||
}
|
||||
}
|
||||
|
||||
type AuthenticationHandlers struct {
|
||||
repository domain.Repository
|
||||
config *config.Config
|
||||
pendingOAuthUsers *cache.Cache
|
||||
repository domain.Repository
|
||||
authProvider provider.AuthProvider
|
||||
config *config.Config
|
||||
systemIAMPolicy *domain.IAMPolicy
|
||||
}
|
||||
|
||||
type AuthFormData struct {
|
||||
AuthMethods []domain.AuthMethod
|
||||
ProviderAvailable bool
|
||||
Csrf string
|
||||
}
|
||||
|
||||
type TailnetSelectionData struct {
|
||||
Tailnets []domain.Tailnet
|
||||
AccountID uint64
|
||||
Tailnets []domain.Tailnet
|
||||
SystemAdmin bool
|
||||
Csrf string
|
||||
}
|
||||
|
||||
type oauthState struct {
|
||||
Key string
|
||||
AuthMethod uint64
|
||||
Key string
|
||||
Flow string
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) StartCliAuth(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
key := c.Param("key")
|
||||
|
||||
if s, err := h.repository.GetAuthenticationRequest(ctx, key); err != nil || s == nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
|
||||
if h.authProvider == nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
|
||||
state, err := h.createState("c", key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
redirectUrl := h.authProvider.GetLoginURL(h.config.CreateUrl("/a/callback"), state)
|
||||
|
||||
return c.Redirect(http.StatusFound, redirectUrl)
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) StartAuth(c echo.Context) error {
|
||||
@@ -56,12 +86,8 @@ func (h *AuthenticationHandlers) StartAuth(c echo.Context) error {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
|
||||
methods, err := h.repository.ListAuthMethods(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, "auth.html", &AuthFormData{AuthMethods: methods})
|
||||
csrf := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
|
||||
return c.Render(http.StatusOK, "auth.html", &AuthFormData{ProviderAvailable: h.authProvider != nil, Csrf: csrf})
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) ProcessAuth(c echo.Context) error {
|
||||
@@ -69,7 +95,7 @@ func (h *AuthenticationHandlers) ProcessAuth(c echo.Context) error {
|
||||
|
||||
key := c.Param("key")
|
||||
authKey := c.FormValue("ak")
|
||||
authMethodId := c.FormValue("s")
|
||||
interactive := c.FormValue("s")
|
||||
|
||||
req, err := h.repository.GetRegistrationRequestByKey(ctx, key)
|
||||
if err != nil || req == nil {
|
||||
@@ -80,28 +106,13 @@ func (h *AuthenticationHandlers) ProcessAuth(c echo.Context) error {
|
||||
return h.endMachineRegistrationFlow(c, req, &oauthState{Key: key})
|
||||
}
|
||||
|
||||
if authMethodId != "" {
|
||||
id, err := strconv.ParseUint(authMethodId, 10, 64)
|
||||
if interactive != "" {
|
||||
state, err := h.createState("r", key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
method, err := h.repository.GetAuthMethod(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state, err := h.createState(key, method.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authProvider, err := provider.NewProvider(method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
redirectUrl := authProvider.GetLoginURL(h.config.CreateUrl("/a/callback"), state)
|
||||
redirectUrl := h.authProvider.GetLoginURL(h.config.CreateUrl("/a/callback"), state)
|
||||
|
||||
return c.Redirect(http.StatusFound, redirectUrl)
|
||||
}
|
||||
@@ -118,30 +129,85 @@ func (h *AuthenticationHandlers) Callback(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := h.exchangeUser(ctx, code, state)
|
||||
user, err := h.exchangeUser(code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filters, err := h.repository.ListAuthFiltersByAuthMethod(ctx, state.AuthMethod)
|
||||
tailnets, err := h.listAvailableTailnets(ctx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tailnets := filters.Evaluate(user.Attr)
|
||||
|
||||
if len(tailnets) == 0 {
|
||||
return c.Redirect(http.StatusFound, "/a/error?e=ua")
|
||||
}
|
||||
|
||||
account, _, err := h.repository.GetOrCreateAccount(ctx, state.AuthMethod, user.ID, user.Name)
|
||||
account, _, err := h.repository.GetOrCreateAccount(ctx, user.ID, user.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.pendingOAuthUsers.Set(state.Key, account, cache.DefaultExpiration)
|
||||
csrf := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
|
||||
|
||||
return c.Render(http.StatusOK, "tailnets.html", &TailnetSelectionData{Tailnets: tailnets})
|
||||
if state.Flow == "r" {
|
||||
if len(tailnets) == 0 {
|
||||
registrationRequest, err := h.repository.GetRegistrationRequestByKey(ctx, state.Key)
|
||||
if err == nil && registrationRequest != nil {
|
||||
registrationRequest.Error = "unauthorized"
|
||||
_ = h.repository.SaveRegistrationRequest(ctx, registrationRequest)
|
||||
}
|
||||
return c.Redirect(http.StatusFound, "/a/error?e=ua")
|
||||
}
|
||||
return c.Render(http.StatusOK, "tailnets.html", &TailnetSelectionData{
|
||||
Csrf: csrf,
|
||||
Tailnets: tailnets,
|
||||
SystemAdmin: false,
|
||||
AccountID: account.ID,
|
||||
})
|
||||
}
|
||||
|
||||
if state.Flow == "c" {
|
||||
isSystemAdmin, err := h.isSystemAdmin(ctx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isSystemAdmin && len(tailnets) == 0 {
|
||||
req, err := h.repository.GetAuthenticationRequest(ctx, state.Key)
|
||||
if err == nil && req != nil {
|
||||
req.Error = "unauthorized"
|
||||
_ = h.repository.SaveAuthenticationRequest(ctx, req)
|
||||
}
|
||||
return c.Redirect(http.StatusFound, "/a/error?e=ua")
|
||||
}
|
||||
return c.Render(http.StatusOK, "tailnets.html", &TailnetSelectionData{
|
||||
Csrf: csrf,
|
||||
Tailnets: tailnets,
|
||||
SystemAdmin: isSystemAdmin,
|
||||
AccountID: account.ID,
|
||||
})
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) isSystemAdmin(ctx context.Context, u *provider.User) (bool, error) {
|
||||
return h.systemIAMPolicy.EvaluatePolicy(&domain.Identity{UserID: u.ID, Email: u.Name, Attr: u.Attr})
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) listAvailableTailnets(ctx context.Context, u *provider.User) ([]domain.Tailnet, error) {
|
||||
var result = []domain.Tailnet{}
|
||||
tailnets, err := h.repository.ListTailnets(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range tailnets {
|
||||
approved, err := t.IAMPolicy.EvaluatePolicy(&domain.Identity{UserID: u.ID, Email: u.Name, Attr: u.Attr})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if approved {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) EndOAuth(c echo.Context) error {
|
||||
@@ -152,12 +218,21 @@ func (h *AuthenticationHandlers) EndOAuth(c echo.Context) error {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
|
||||
req, err := h.repository.GetRegistrationRequestByKey(ctx, state.Key)
|
||||
if state.Flow == "r" {
|
||||
req, err := h.repository.GetRegistrationRequestByKey(ctx, state.Key)
|
||||
if err != nil || req == nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
|
||||
return h.endMachineRegistrationFlow(c, req, state)
|
||||
}
|
||||
|
||||
req, err := h.repository.GetAuthenticationRequest(ctx, state.Key)
|
||||
if err != nil || req == nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
|
||||
return h.endMachineRegistrationFlow(c, req, state)
|
||||
return h.endCliAuthenticationFlow(c, req, state)
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) Success(c echo.Context) error {
|
||||
@@ -171,15 +246,91 @@ func (h *AuthenticationHandlers) Error(c echo.Context) error {
|
||||
return c.Render(http.StatusForbidden, "invalidauthkey.html", nil)
|
||||
case "ua":
|
||||
return c.Render(http.StatusForbidden, "unauthorized.html", nil)
|
||||
case "nto":
|
||||
return c.Render(http.StatusForbidden, "notagowner.html", nil)
|
||||
}
|
||||
return c.Render(http.StatusOK, "error.html", nil)
|
||||
}
|
||||
|
||||
type TailnetSelectionForm struct {
|
||||
AccountID uint64 `form:"aid"`
|
||||
TailnetID uint64 `form:"tid"`
|
||||
AsSystemAdmin bool `form:"sad"`
|
||||
AuthKey string `form:"ak"`
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) endCliAuthenticationFlow(c echo.Context, req *domain.AuthenticationRequest, state *oauthState) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var form TailnetSelectionForm
|
||||
if err := c.Bind(&form); err != nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
|
||||
account, err := h.repository.GetAccount(ctx, form.AccountID)
|
||||
if err != nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
|
||||
// continue as system admin?
|
||||
if form.AsSystemAdmin {
|
||||
expiresAt := time.Now().Add(24 * time.Hour)
|
||||
token, apiKey := domain.CreateSystemApiKey(account, &expiresAt)
|
||||
req.Token = token
|
||||
|
||||
err := h.repository.Transaction(func(rp domain.Repository) error {
|
||||
if err := rp.SaveSystemApiKey(ctx, apiKey); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rp.SaveAuthenticationRequest(ctx, req); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Redirect(http.StatusFound, "/a/success")
|
||||
}
|
||||
|
||||
tailnet, err := h.repository.GetTailnet(ctx, form.TailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, _, err := h.repository.GetOrCreateUserWithAccount(ctx, tailnet, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expiresAt := time.Now().Add(24 * time.Hour)
|
||||
token, apiKey := domain.CreateApiKey(tailnet, user, &expiresAt)
|
||||
req.Token = token
|
||||
req.TailnetID = &tailnet.ID
|
||||
|
||||
err = h.repository.Transaction(func(rp domain.Repository) error {
|
||||
if err := rp.SaveApiKey(ctx, apiKey); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rp.SaveAuthenticationRequest(ctx, req); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusFound, "/a/success")
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, registrationRequest *domain.RegistrationRequest, state *oauthState) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
authKeyParam := c.FormValue("ak")
|
||||
tailnetIDParam := c.FormValue("s")
|
||||
var form TailnetSelectionForm
|
||||
if err := c.Bind(&form); err != nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
|
||||
req := tailcfg.RegisterRequest(registrationRequest.Data)
|
||||
machineKey := registrationRequest.MachineKey
|
||||
@@ -190,8 +341,8 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
var ephemeral bool
|
||||
var tags = []string{}
|
||||
|
||||
if authKeyParam != "" {
|
||||
authKey, err := h.repository.LoadAuthKey(ctx, authKeyParam)
|
||||
if form.AuthKey != "" {
|
||||
authKey, err := h.repository.LoadAuthKey(ctx, form.AuthKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -213,30 +364,37 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
tags = authKey.Tags
|
||||
ephemeral = authKey.Ephemeral
|
||||
} else {
|
||||
parseUint, err := strconv.ParseUint(tailnetIDParam, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tailnet, err = h.repository.GetTailnet(ctx, parseUint)
|
||||
selectedTailnet, err := h.repository.GetTailnet(ctx, form.TailnetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
item, ok := h.pendingOAuthUsers.Get(state.Key)
|
||||
if !ok {
|
||||
account, err := h.repository.GetAccount(ctx, form.AccountID)
|
||||
if err != nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
|
||||
oa := item.(*domain.Account)
|
||||
|
||||
user, _, err = h.repository.GetOrCreateUserWithAccount(ctx, tailnet, oa)
|
||||
selectedUser, _, err := h.repository.GetOrCreateUserWithAccount(ctx, selectedTailnet, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user = selectedUser
|
||||
tailnet = selectedTailnet
|
||||
ephemeral = false
|
||||
}
|
||||
|
||||
if err := tailnet.ACLPolicy.CheckTagOwners(registrationRequest.Data.Hostinfo.RequestTags, user); err != nil {
|
||||
registrationRequest.Authenticated = false
|
||||
registrationRequest.Error = err.Error()
|
||||
if err := h.repository.SaveRegistrationRequest(ctx, registrationRequest); err != nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error")
|
||||
}
|
||||
return c.Redirect(http.StatusFound, "/a/error?e=nto")
|
||||
}
|
||||
|
||||
autoAllowIPs := tailnet.ACLPolicy.FindAutoApprovedIPs(req.Hostinfo.RoutableIPs, tags, user)
|
||||
|
||||
var m *domain.Machine
|
||||
|
||||
m, err := h.repository.GetMachineByKey(ctx, tailnet.ID, machineKey)
|
||||
@@ -244,20 +402,13 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
return err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
now := time.Now().UTC()
|
||||
now := time.Now().UTC()
|
||||
|
||||
if m == nil {
|
||||
registeredTags := tags
|
||||
advertisedTags := domain.SanitizeTags(req.Hostinfo.RequestTags)
|
||||
tags := append(registeredTags, advertisedTags...)
|
||||
|
||||
if len(tags) != 0 {
|
||||
user, _, err = h.repository.GetOrCreateServiceUser(ctx, tailnet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sanitizeHostname := dnsname.SanitizeHostname(req.Hostinfo.Hostname)
|
||||
nameIdx, err := h.repository.GetNextMachineNameIndex(ctx, tailnet.ID, sanitizeHostname)
|
||||
if err != nil {
|
||||
@@ -265,42 +416,34 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
}
|
||||
|
||||
m = &domain.Machine{
|
||||
ID: util.NextID(),
|
||||
Name: sanitizeHostname,
|
||||
NameIdx: nameIdx,
|
||||
MachineKey: machineKey,
|
||||
NodeKey: nodeKey,
|
||||
Ephemeral: ephemeral,
|
||||
RegisteredTags: registeredTags,
|
||||
Tags: domain.SanitizeTags(tags),
|
||||
CreatedAt: now,
|
||||
ID: util.NextID(),
|
||||
Name: sanitizeHostname,
|
||||
NameIdx: nameIdx,
|
||||
MachineKey: machineKey,
|
||||
NodeKey: nodeKey,
|
||||
Ephemeral: ephemeral || req.Ephemeral,
|
||||
RegisteredTags: registeredTags,
|
||||
Tags: domain.SanitizeTags(tags),
|
||||
AutoAllowIPs: autoAllowIPs,
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now.Add(180 * 24 * time.Hour).UTC(),
|
||||
KeyExpiryDisabled: len(tags) != 0,
|
||||
|
||||
User: *user,
|
||||
Tailnet: *tailnet,
|
||||
}
|
||||
|
||||
if !req.Expiry.IsZero() {
|
||||
m.ExpiresAt = &req.Expiry
|
||||
}
|
||||
|
||||
ipv4, ipv6, err := addr.SelectIP(checkIP(ctx, h.repository.CountMachinesWithIPv4))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.IPv4 = domain.IP{IP: ipv4}
|
||||
m.IPv6 = domain.IP{IP: ipv6}
|
||||
m.IPv4 = domain.IP{Addr: ipv4}
|
||||
m.IPv6 = domain.IP{Addr: ipv6}
|
||||
} else {
|
||||
registeredTags := tags
|
||||
advertisedTags := domain.SanitizeTags(req.Hostinfo.RequestTags)
|
||||
tags := append(registeredTags, advertisedTags...)
|
||||
|
||||
if len(tags) != 0 {
|
||||
user, _, err = h.repository.GetOrCreateServiceUser(ctx, tailnet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sanitizeHostname := dnsname.SanitizeHostname(req.Hostinfo.Hostname)
|
||||
if m.Name != sanitizeHostname {
|
||||
nameIdx, err := h.repository.GetNextMachineNameIndex(ctx, tailnet.ID, sanitizeHostname)
|
||||
@@ -311,14 +454,15 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
m.NameIdx = nameIdx
|
||||
}
|
||||
m.NodeKey = nodeKey
|
||||
m.Ephemeral = ephemeral
|
||||
m.Ephemeral = ephemeral || req.Ephemeral
|
||||
m.RegisteredTags = registeredTags
|
||||
m.Tags = domain.SanitizeTags(tags)
|
||||
m.AutoAllowIPs = autoAllowIPs
|
||||
m.UserID = user.ID
|
||||
m.User = *user
|
||||
m.TailnetID = tailnet.ID
|
||||
m.Tailnet = *tailnet
|
||||
m.ExpiresAt = nil
|
||||
m.ExpiresAt = now.Add(180 * 24 * time.Hour).UTC()
|
||||
}
|
||||
|
||||
err = h.repository.Transaction(func(rp domain.Repository) error {
|
||||
@@ -343,23 +487,10 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
return c.Redirect(http.StatusFound, "/a/success")
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) getAuthProvider(ctx context.Context, authMethodId uint64) (provider.AuthProvider, error) {
|
||||
authMethod, err := h.repository.GetAuthMethod(ctx, authMethodId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return provider.NewProvider(authMethod)
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) exchangeUser(ctx context.Context, code string, state *oauthState) (*provider.User, error) {
|
||||
func (h *AuthenticationHandlers) exchangeUser(code string) (*provider.User, error) {
|
||||
redirectUrl := h.config.CreateUrl("/a/callback")
|
||||
|
||||
authProvider, err := h.getAuthProvider(ctx, state.AuthMethod)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := authProvider.Exchange(redirectUrl, code)
|
||||
user, err := h.authProvider.Exchange(redirectUrl, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -367,8 +498,8 @@ func (h *AuthenticationHandlers) exchangeUser(ctx context.Context, code string,
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) createState(key string, authMethodId uint64) (string, error) {
|
||||
stateMap := oauthState{Key: key, AuthMethod: authMethodId}
|
||||
func (h *AuthenticationHandlers) createState(flow string, key string) (string, error) {
|
||||
stateMap := oauthState{Key: key, Flow: flow}
|
||||
marshal, err := json.Marshal(&stateMap)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -4,12 +4,32 @@ import (
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func httpsRedirectSkipper(c config.Tls) func(ctx echo.Context) bool {
|
||||
return func(ctx echo.Context) bool {
|
||||
if ctx.Request().Method == "POST" && ctx.Request().RequestURI == "/ts2021" {
|
||||
return true
|
||||
}
|
||||
return !c.ForceHttps
|
||||
}
|
||||
}
|
||||
|
||||
func HttpsRedirect(c config.Tls) echo.MiddlewareFunc {
|
||||
return middleware.HTTPSRedirectWithConfig(middleware.RedirectConfig{
|
||||
Skipper: httpsRedirectSkipper(c),
|
||||
})
|
||||
}
|
||||
|
||||
func HttpRedirectHandler(tls config.Tls) echo.HandlerFunc {
|
||||
if tls.CertMagicDomain != "" {
|
||||
if tls.Disable {
|
||||
return IndexHandler(http.StatusNotFound)
|
||||
}
|
||||
|
||||
if tls.AcmeEnabled {
|
||||
cfg := certmagic.NewDefault()
|
||||
if len(cfg.Issuers) > 0 {
|
||||
if am, ok := cfg.Issuers[0].(*certmagic.ACMEIssuer); ok {
|
||||
@@ -19,10 +39,6 @@ func HttpRedirectHandler(tls config.Tls) echo.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
if tls.Disable {
|
||||
return IndexHandler(http.StatusNotFound)
|
||||
}
|
||||
|
||||
return echo.WrapHandler(http.HandlerFunc(httpRedirectHandler))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -12,7 +12,7 @@ const (
|
||||
NoiseCapabilityVersion = 28
|
||||
)
|
||||
|
||||
func KeyHandler(keys *domain.ControlKeys) echo.HandlerFunc {
|
||||
func KeyHandler(keys *config.ServerKeys) echo.HandlerFunc {
|
||||
legacyPublicKey := keys.LegacyControlKey.Public()
|
||||
publicKey := keys.ControlKey.Public()
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
const prometheusNamespace = "ionscale"
|
||||
|
||||
var (
|
||||
connectedDevices = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: prometheusNamespace,
|
||||
Name: "connected_machines_total",
|
||||
Help: "Total amount of connected machines",
|
||||
}, []string{"tailnet"})
|
||||
)
|
||||
@@ -1,7 +1,6 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/labstack/echo/v4"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
@@ -26,7 +25,7 @@ func NewNoiseHandlers(controlKey key.MachinePrivate, createPeerHandler CreatePee
|
||||
}
|
||||
|
||||
func (h *NoiseHandlers) Upgrade(c echo.Context) error {
|
||||
conn, err := controlhttp.AcceptHTTP(context.Background(), c.Response(), c.Request(), h.controlKey)
|
||||
conn, err := controlhttp.AcceptHTTP(c.Request().Context(), c.Response(), c.Request(), h.controlKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,28 +4,24 @@ import (
|
||||
"context"
|
||||
"github.com/jsiebens/ionscale/internal/bind"
|
||||
"github.com/jsiebens/ionscale/internal/broker"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/mapping"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/dnsname"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
keepAliveInterval = 1 * time.Minute
|
||||
)
|
||||
|
||||
func NewPollNetMapHandler(
|
||||
createBinder bind.Factory,
|
||||
brokers *broker.BrokerPool,
|
||||
brokers broker.Pubsub,
|
||||
repository domain.Repository,
|
||||
offlineTimers *OfflineTimers) *PollNetMapHandler {
|
||||
|
||||
handler := &PollNetMapHandler{
|
||||
createBinder: createBinder,
|
||||
brokers: brokers.Get,
|
||||
brokers: brokers,
|
||||
repository: repository,
|
||||
offlineTimers: offlineTimers,
|
||||
}
|
||||
@@ -36,7 +32,7 @@ func NewPollNetMapHandler(
|
||||
type PollNetMapHandler struct {
|
||||
createBinder bind.Factory
|
||||
repository domain.Repository
|
||||
brokers func(uint64) broker.Broker
|
||||
brokers broker.Pubsub
|
||||
offlineTimers *OfflineTimers
|
||||
}
|
||||
|
||||
@@ -89,8 +85,7 @@ func (h *PollNetMapHandler) handleUpdate(c echo.Context, binder bind.Binder, m *
|
||||
tailnetID := m.TailnetID
|
||||
machineID := m.ID
|
||||
|
||||
tailnetBroker := h.brokers(tailnetID)
|
||||
tailnetBroker.SignalPeerUpdated(machineID)
|
||||
h.brokers.Publish(tailnetID, &broker.Signal{PeerUpdated: &machineID})
|
||||
|
||||
if !mapRequest.Stream {
|
||||
return c.String(http.StatusOK, "")
|
||||
@@ -104,9 +99,11 @@ func (h *PollNetMapHandler) handleUpdate(c echo.Context, binder bind.Binder, m *
|
||||
}
|
||||
|
||||
updateChan := make(chan *broker.Signal, 20)
|
||||
client := broker.NewClient(machineID, updateChan)
|
||||
|
||||
tailnetBroker.AddClient(&client)
|
||||
unsubscribe, err := h.brokers.Subscribe(tailnetID, updateChan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.cancelOfflineMessage(machineID)
|
||||
|
||||
// Listen to connection close
|
||||
@@ -116,7 +113,7 @@ func (h *PollNetMapHandler) handleUpdate(c echo.Context, binder bind.Binder, m *
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keepAliveTicker := time.NewTicker(keepAliveInterval)
|
||||
keepAliveTicker := time.NewTicker(config.KeepAliveInterval())
|
||||
syncTicker := time.NewTicker(5 * time.Second)
|
||||
|
||||
c.Response().WriteHeader(http.StatusOK)
|
||||
@@ -126,8 +123,11 @@ func (h *PollNetMapHandler) handleUpdate(c echo.Context, binder bind.Binder, m *
|
||||
}
|
||||
c.Response().Flush()
|
||||
|
||||
connectedDevices.WithLabelValues(m.Tailnet.Name).Inc()
|
||||
|
||||
defer func() {
|
||||
tailnetBroker.RemoveClient(machineID)
|
||||
connectedDevices.WithLabelValues(m.Tailnet.Name).Dec()
|
||||
unsubscribe()
|
||||
keepAliveTicker.Stop()
|
||||
syncTicker.Stop()
|
||||
_ = h.repository.SetMachineLastSeen(ctx, machineID)
|
||||
@@ -219,16 +219,17 @@ func (h *PollNetMapHandler) createKeepAliveResponse(binder bind.Binder, request
|
||||
func (h *PollNetMapHandler) createMapResponse(m *domain.Machine, binder bind.Binder, request *tailcfg.MapRequest, delta bool, prevSyncedPeerIDs map[uint64]bool) ([]byte, map[uint64]bool, error) {
|
||||
ctx := context.TODO()
|
||||
|
||||
node, user, err := mapping.ToNode(m, true)
|
||||
node, user, err := mapping.ToNode(m)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
policies, err := h.repository.GetACLPolicy(ctx, m.TailnetID)
|
||||
tailnet, err := h.repository.GetTailnet(ctx, m.TailnetID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
policies := tailnet.ACLPolicy
|
||||
var users = []tailcfg.UserProfile{*user}
|
||||
var changedPeers []*tailcfg.Node
|
||||
var removedPeers []tailcfg.NodeID
|
||||
@@ -245,8 +246,8 @@ func (h *PollNetMapHandler) createMapResponse(m *domain.Machine, binder bind.Bin
|
||||
if peer.IsExpired() {
|
||||
continue
|
||||
}
|
||||
if domain.IsValidPeer(policies, m, &peer) || domain.IsValidPeer(policies, &peer, m) {
|
||||
n, u, err := mapping.ToNode(&peer, h.brokers(peer.TailnetID).IsConnected(peer.ID))
|
||||
if policies.IsValidPeer(m, &peer) || policies.IsValidPeer(&peer, m) {
|
||||
n, u, err := mapping.ToNode(&peer)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -265,17 +266,14 @@ func (h *PollNetMapHandler) createMapResponse(m *domain.Machine, binder bind.Bin
|
||||
removedPeers = append(removedPeers, tailcfg.NodeID(p))
|
||||
}
|
||||
|
||||
dnsConfig, err := h.repository.GetDNSConfig(ctx, m.TailnetID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
dnsConfig := tailnet.DNSConfig
|
||||
|
||||
derpMap, err := h.repository.GetDERPMap(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rules := domain.BuildFilterRules(policies, m, candidatePeers)
|
||||
rules := policies.BuildFilterRules(candidatePeers, m)
|
||||
|
||||
controlTime := time.Now().UTC()
|
||||
var mapResponse *tailcfg.MapResponse
|
||||
@@ -284,19 +282,24 @@ func (h *PollNetMapHandler) createMapResponse(m *domain.Machine, binder bind.Bin
|
||||
mapResponse = &tailcfg.MapResponse{
|
||||
KeepAlive: false,
|
||||
Node: node,
|
||||
DNSConfig: mapping.ToDNSConfig(&m.Tailnet, dnsConfig),
|
||||
DNSConfig: mapping.ToDNSConfig(&m.Tailnet, &dnsConfig),
|
||||
PacketFilter: rules,
|
||||
DERPMap: derpMap,
|
||||
Domain: dnsname.SanitizeHostname(m.Tailnet.Name),
|
||||
Domain: domain.SanitizeTailnetName(m.Tailnet.Name),
|
||||
Peers: changedPeers,
|
||||
UserProfiles: users,
|
||||
ControlTime: &controlTime,
|
||||
Debug: &tailcfg.Debug{
|
||||
DisableLogTail: true,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
mapResponse = &tailcfg.MapResponse{
|
||||
Node: node,
|
||||
DNSConfig: mapping.ToDNSConfig(&m.Tailnet, &dnsConfig),
|
||||
PacketFilter: rules,
|
||||
DERPMap: derpMap,
|
||||
Domain: domain.SanitizeTailnetName(m.Tailnet.Name),
|
||||
PeersChanged: changedPeers,
|
||||
PeersRemoved: removedPeers,
|
||||
UserProfiles: users,
|
||||
@@ -315,10 +318,10 @@ func (h *PollNetMapHandler) createMapResponse(m *domain.Machine, binder bind.Bin
|
||||
return payload, syncedPeerIDs, nil
|
||||
}
|
||||
|
||||
func NewOfflineTimers(repository domain.Repository, brokers *broker.BrokerPool) *OfflineTimers {
|
||||
func NewOfflineTimers(repository domain.Repository, pubsub broker.Pubsub) *OfflineTimers {
|
||||
return &OfflineTimers{
|
||||
repository: repository,
|
||||
brokers: brokers.Get,
|
||||
pubsub: pubsub,
|
||||
data: make(map[uint64]*time.Timer),
|
||||
startCh: make(chan [2]uint64),
|
||||
stopCh: make(chan uint64),
|
||||
@@ -327,7 +330,7 @@ func NewOfflineTimers(repository domain.Repository, brokers *broker.BrokerPool)
|
||||
|
||||
type OfflineTimers struct {
|
||||
repository domain.Repository
|
||||
brokers func(uint64) broker.Broker
|
||||
pubsub broker.Pubsub
|
||||
data map[uint64]*time.Timer
|
||||
stopCh chan uint64
|
||||
startCh chan [2]uint64
|
||||
@@ -351,13 +354,11 @@ func (o *OfflineTimers) scheduleOfflineMessage(tailnetID, machineID uint64) {
|
||||
delete(o.data, machineID)
|
||||
}
|
||||
|
||||
timer := time.NewTimer(10 * time.Second)
|
||||
timer := time.NewTimer(config.KeepAliveInterval())
|
||||
go func() {
|
||||
<-timer.C
|
||||
if !o.brokers(tailnetID).IsConnected(machineID) {
|
||||
o.brokers(tailnetID).SignalPeerUpdated(machineID)
|
||||
o.stopCh <- machineID
|
||||
}
|
||||
o.pubsub.Publish(tailnetID, &broker.Signal{PeerUpdated: &machineID})
|
||||
o.stopCh <- machineID
|
||||
}()
|
||||
|
||||
o.data[machineID] = timer
|
||||
|
||||
@@ -12,15 +12,15 @@ const (
|
||||
inactivityTimeout = 30 * time.Minute
|
||||
)
|
||||
|
||||
func NewReaper(brokers *broker.BrokerPool, repository domain.Repository) *Reaper {
|
||||
func NewReaper(brokers broker.Pubsub, repository domain.Repository) *Reaper {
|
||||
return &Reaper{
|
||||
brokers: brokers,
|
||||
pubsub: brokers,
|
||||
repository: repository,
|
||||
}
|
||||
}
|
||||
|
||||
type Reaper struct {
|
||||
brokers *broker.BrokerPool
|
||||
pubsub broker.Pubsub
|
||||
repository domain.Repository
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func (r *Reaper) reapInactiveEphemeralNodes() {
|
||||
|
||||
if len(removedNodes) != 0 {
|
||||
for i, p := range removedNodes {
|
||||
r.brokers.Get(i).SignalPeersRemoved(p)
|
||||
r.pubsub.Publish(i, &broker.Signal{PeersRemoved: p})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"github.com/labstack/echo/v4"
|
||||
"inet.af/netaddr"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/dnsname"
|
||||
"time"
|
||||
@@ -19,11 +19,11 @@ import (
|
||||
func NewRegistrationHandlers(
|
||||
createBinder bind.Factory,
|
||||
config *config.Config,
|
||||
brokers *broker.BrokerPool,
|
||||
brokers broker.Pubsub,
|
||||
repository domain.Repository) *RegistrationHandlers {
|
||||
return &RegistrationHandlers{
|
||||
createBinder: createBinder,
|
||||
brokers: brokers.Get,
|
||||
pubsub: brokers,
|
||||
repository: repository,
|
||||
config: config,
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func NewRegistrationHandlers(
|
||||
type RegistrationHandlers struct {
|
||||
createBinder bind.Factory
|
||||
repository domain.Repository
|
||||
brokers func(uint64) broker.Broker
|
||||
pubsub broker.Pubsub
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
@@ -60,19 +60,19 @@ func (h *RegistrationHandlers) Register(c echo.Context) error {
|
||||
}
|
||||
|
||||
if m != nil {
|
||||
if m.ExpiresAt != nil && !m.ExpiresAt.IsZero() && m.ExpiresAt.Before(time.Now()) {
|
||||
if m.IsExpired() {
|
||||
response := tailcfg.RegisterResponse{NodeKeyExpired: true}
|
||||
return binder.WriteResponse(c, http.StatusOK, response)
|
||||
}
|
||||
|
||||
if !req.Expiry.IsZero() && req.Expiry.Before(time.Now()) {
|
||||
m.ExpiresAt = &req.Expiry
|
||||
m.ExpiresAt = req.Expiry
|
||||
|
||||
if err := h.repository.SaveMachine(ctx, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.brokers(m.TailnetID).SignalPeerUpdated(m.ID)
|
||||
h.pubsub.Publish(m.TailnetID, &broker.Signal{PeerUpdated: &m.ID})
|
||||
|
||||
response := tailcfg.RegisterResponse{NodeKeyExpired: true}
|
||||
return binder.WriteResponse(c, http.StatusOK, response)
|
||||
@@ -151,6 +151,17 @@ func (h *RegistrationHandlers) authenticateMachineWithAuthKey(c echo.Context, bi
|
||||
tailnet := authKey.Tailnet
|
||||
user := authKey.User
|
||||
|
||||
if err := tailnet.ACLPolicy.CheckTagOwners(req.Hostinfo.RequestTags, &user); err != nil {
|
||||
response := tailcfg.RegisterResponse{MachineAuthorized: false, Error: err.Error()}
|
||||
return binder.WriteResponse(c, http.StatusOK, response)
|
||||
}
|
||||
|
||||
registeredTags := authKey.Tags
|
||||
advertisedTags := domain.SanitizeTags(req.Hostinfo.RequestTags)
|
||||
tags := append(registeredTags, advertisedTags...)
|
||||
|
||||
autoAllowIPs := tailnet.ACLPolicy.FindAutoApprovedIPs(req.Hostinfo.RoutableIPs, tags, &user)
|
||||
|
||||
var m *domain.Machine
|
||||
|
||||
m, err = h.repository.GetMachineByKey(ctx, tailnet.ID, machineKey)
|
||||
@@ -158,13 +169,9 @@ func (h *RegistrationHandlers) authenticateMachineWithAuthKey(c echo.Context, bi
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
if m == nil {
|
||||
now := time.Now().UTC()
|
||||
|
||||
registeredTags := authKey.Tags
|
||||
advertisedTags := domain.SanitizeTags(req.Hostinfo.RequestTags)
|
||||
tags := append(registeredTags, advertisedTags...)
|
||||
|
||||
sanitizeHostname := dnsname.SanitizeHostname(req.Hostinfo.Hostname)
|
||||
nameIdx, err := h.repository.GetNextMachineNameIndex(ctx, tailnet.ID, sanitizeHostname)
|
||||
if err != nil {
|
||||
@@ -172,35 +179,34 @@ func (h *RegistrationHandlers) authenticateMachineWithAuthKey(c echo.Context, bi
|
||||
}
|
||||
|
||||
m = &domain.Machine{
|
||||
ID: util.NextID(),
|
||||
Name: sanitizeHostname,
|
||||
NameIdx: nameIdx,
|
||||
MachineKey: machineKey,
|
||||
NodeKey: nodeKey,
|
||||
Ephemeral: authKey.Ephemeral,
|
||||
RegisteredTags: registeredTags,
|
||||
Tags: domain.SanitizeTags(tags),
|
||||
CreatedAt: now,
|
||||
ID: util.NextID(),
|
||||
Name: sanitizeHostname,
|
||||
NameIdx: nameIdx,
|
||||
MachineKey: machineKey,
|
||||
NodeKey: nodeKey,
|
||||
Ephemeral: authKey.Ephemeral || req.Ephemeral,
|
||||
RegisteredTags: registeredTags,
|
||||
Tags: domain.SanitizeTags(tags),
|
||||
AutoAllowIPs: autoAllowIPs,
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now.Add(180 * 24 * time.Hour).UTC(),
|
||||
KeyExpiryDisabled: len(tags) != 0,
|
||||
|
||||
User: user,
|
||||
Tailnet: tailnet,
|
||||
}
|
||||
|
||||
if !req.Expiry.IsZero() {
|
||||
m.ExpiresAt = &req.Expiry
|
||||
m.ExpiresAt = req.Expiry
|
||||
}
|
||||
|
||||
ipv4, ipv6, err := addr.SelectIP(checkIP(ctx, h.repository.CountMachinesWithIPv4))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.IPv4 = domain.IP{IP: ipv4}
|
||||
m.IPv6 = domain.IP{IP: ipv6}
|
||||
m.IPv4 = domain.IP{Addr: ipv4}
|
||||
m.IPv6 = domain.IP{Addr: ipv6}
|
||||
} else {
|
||||
registeredTags := authKey.Tags
|
||||
advertisedTags := domain.SanitizeTags(req.Hostinfo.RequestTags)
|
||||
tags := append(registeredTags, advertisedTags...)
|
||||
|
||||
sanitizeHostname := dnsname.SanitizeHostname(req.Hostinfo.Hostname)
|
||||
if m.Name != sanitizeHostname {
|
||||
nameIdx, err := h.repository.GetNextMachineNameIndex(ctx, tailnet.ID, sanitizeHostname)
|
||||
@@ -211,14 +217,15 @@ func (h *RegistrationHandlers) authenticateMachineWithAuthKey(c echo.Context, bi
|
||||
m.NameIdx = nameIdx
|
||||
}
|
||||
m.NodeKey = nodeKey
|
||||
m.Ephemeral = authKey.Ephemeral
|
||||
m.Ephemeral = authKey.Ephemeral || req.Ephemeral
|
||||
m.RegisteredTags = registeredTags
|
||||
m.Tags = domain.SanitizeTags(tags)
|
||||
m.AutoAllowIPs = autoAllowIPs
|
||||
m.UserID = user.ID
|
||||
m.User = user
|
||||
m.TailnetID = tailnet.ID
|
||||
m.Tailnet = tailnet
|
||||
m.ExpiresAt = nil
|
||||
m.ExpiresAt = now.Add(180 * 24 * time.Hour).UTC()
|
||||
}
|
||||
|
||||
if err := h.repository.SaveMachine(ctx, m); err != nil {
|
||||
@@ -260,7 +267,7 @@ func (h *RegistrationHandlers) followup(c echo.Context, binder bind.Binder, req
|
||||
}
|
||||
|
||||
func checkIP(cxt context.Context, s Selector) addr.Predicate {
|
||||
return func(ip netaddr.IP) (bool, error) {
|
||||
return func(ip netip.Addr) (bool, error) {
|
||||
c, err := s(cxt, ip.String())
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
+33
-28
@@ -3,18 +3,17 @@ package mapping
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"inet.af/netaddr"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/dnsname"
|
||||
"time"
|
||||
)
|
||||
|
||||
const NetworkMagicDNSSuffix = "ionscale.net"
|
||||
|
||||
func CopyViaJson[F any, T any](f F, t T) error {
|
||||
raw, err := json.Marshal(f)
|
||||
if err != nil {
|
||||
@@ -29,50 +28,50 @@ func CopyViaJson[F any, T any](f F, t T) error {
|
||||
}
|
||||
|
||||
func ToDNSConfig(tailnet *domain.Tailnet, c *domain.DNSConfig) *tailcfg.DNSConfig {
|
||||
tailnetDomain := dnsname.SanitizeHostname(tailnet.Name)
|
||||
resolvers := []dnstype.Resolver{}
|
||||
tailnetDomain := domain.SanitizeTailnetName(tailnet.Name)
|
||||
resolvers := []*dnstype.Resolver{}
|
||||
for _, r := range c.Nameservers {
|
||||
resolver := dnstype.Resolver{
|
||||
resolver := &dnstype.Resolver{
|
||||
Addr: r,
|
||||
}
|
||||
resolvers = append(resolvers, resolver)
|
||||
}
|
||||
|
||||
config := &tailcfg.DNSConfig{}
|
||||
dnsConfig := &tailcfg.DNSConfig{}
|
||||
|
||||
var domains []string
|
||||
|
||||
if c.MagicDNS {
|
||||
domains = append(domains, fmt.Sprintf("%s.%s", tailnetDomain, NetworkMagicDNSSuffix))
|
||||
config.Proxied = true
|
||||
domains = append(domains, fmt.Sprintf("%s.%s", tailnetDomain, config.MagicDNSSuffix()))
|
||||
dnsConfig.Proxied = true
|
||||
}
|
||||
|
||||
if c.OverrideLocalDNS {
|
||||
config.Resolvers = resolvers
|
||||
dnsConfig.Resolvers = resolvers
|
||||
} else {
|
||||
config.FallbackResolvers = resolvers
|
||||
dnsConfig.FallbackResolvers = resolvers
|
||||
}
|
||||
|
||||
if len(c.Routes) != 0 {
|
||||
routes := make(map[string][]dnstype.Resolver)
|
||||
routes := make(map[string][]*dnstype.Resolver)
|
||||
for r, s := range c.Routes {
|
||||
routeResolver := []dnstype.Resolver{}
|
||||
routeResolver := []*dnstype.Resolver{}
|
||||
for _, addr := range s {
|
||||
resolver := dnstype.Resolver{Addr: addr}
|
||||
resolver := &dnstype.Resolver{Addr: addr}
|
||||
routeResolver = append(routeResolver, resolver)
|
||||
}
|
||||
routes[r] = routeResolver
|
||||
domains = append(domains, r)
|
||||
}
|
||||
config.Routes = routes
|
||||
dnsConfig.Routes = routes
|
||||
}
|
||||
|
||||
config.Domains = domains
|
||||
dnsConfig.Domains = domains
|
||||
|
||||
return config
|
||||
return dnsConfig
|
||||
}
|
||||
|
||||
func ToNode(m *domain.Machine, connected bool) (*tailcfg.Node, *tailcfg.UserProfile, error) {
|
||||
func ToNode(m *domain.Machine) (*tailcfg.Node, *tailcfg.UserProfile, error) {
|
||||
nKey, err := util.ParseNodePublicKey(m.NodeKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -95,10 +94,10 @@ func ToNode(m *domain.Machine, connected bool) (*tailcfg.Node, *tailcfg.UserProf
|
||||
endpoints := m.Endpoints
|
||||
hostinfo := tailcfg.Hostinfo(m.HostInfo)
|
||||
|
||||
var addrs []netaddr.IPPrefix
|
||||
var allowedIPs []netaddr.IPPrefix
|
||||
var addrs []netip.Prefix
|
||||
var allowedIPs []netip.Prefix
|
||||
|
||||
if !m.IPv4.IsZero() {
|
||||
if m.IPv4.IsValid() {
|
||||
ipv4, err := m.IPv4.Prefix(32)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -107,7 +106,7 @@ func ToNode(m *domain.Machine, connected bool) (*tailcfg.Node, *tailcfg.UserProf
|
||||
allowedIPs = append(allowedIPs, ipv4)
|
||||
}
|
||||
|
||||
if !m.IPv6.IsZero() {
|
||||
if m.IPv6.IsValid() {
|
||||
ipv6, err := m.IPv6.Prefix(128)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -117,6 +116,7 @@ func ToNode(m *domain.Machine, connected bool) (*tailcfg.Node, *tailcfg.UserProf
|
||||
}
|
||||
|
||||
allowedIPs = append(allowedIPs, m.AllowIPs...)
|
||||
allowedIPs = append(allowedIPs, m.AutoAllowIPs...)
|
||||
|
||||
var derp string
|
||||
if hostinfo.NetInfo != nil {
|
||||
@@ -130,7 +130,7 @@ func ToNode(m *domain.Machine, connected bool) (*tailcfg.Node, *tailcfg.UserProf
|
||||
name = fmt.Sprintf("%s-%d", m.Name, m.NameIdx)
|
||||
}
|
||||
|
||||
sanitizedTailnetName := dnsname.SanitizeHostname(m.Tailnet.Name)
|
||||
sanitizedTailnetName := domain.SanitizeTailnetName(m.Tailnet.Name)
|
||||
|
||||
hostInfo := tailcfg.Hostinfo{
|
||||
OS: hostinfo.OS,
|
||||
@@ -141,7 +141,7 @@ func ToNode(m *domain.Machine, connected bool) (*tailcfg.Node, *tailcfg.UserProf
|
||||
n := tailcfg.Node{
|
||||
ID: tailcfg.NodeID(m.ID),
|
||||
StableID: tailcfg.StableNodeID(strconv.FormatUint(m.ID, 10)),
|
||||
Name: fmt.Sprintf("%s.%s.%s.", name, sanitizedTailnetName, NetworkMagicDNSSuffix),
|
||||
Name: fmt.Sprintf("%s.%s.%s.", name, sanitizedTailnetName, config.MagicDNSSuffix()),
|
||||
Key: *nKey,
|
||||
Machine: *mKey,
|
||||
DiscoKey: discoKey,
|
||||
@@ -158,15 +158,20 @@ func ToNode(m *domain.Machine, connected bool) (*tailcfg.Node, *tailcfg.UserProf
|
||||
User: tailcfg.UserID(m.UserID),
|
||||
}
|
||||
|
||||
if m.ExpiresAt != nil {
|
||||
if !m.ExpiresAt.IsZero() {
|
||||
e := m.ExpiresAt.UTC()
|
||||
n.KeyExpiry = e
|
||||
}
|
||||
|
||||
n.Online = &connected
|
||||
if !connected && m.LastSeen != nil {
|
||||
if m.KeyExpiryDisabled {
|
||||
n.KeyExpiry = time.Time{}
|
||||
}
|
||||
|
||||
if m.LastSeen != nil {
|
||||
l := m.LastSeen.UTC()
|
||||
online := m.LastSeen.After(time.Now().Add(-config.KeepAliveInterval()))
|
||||
n.LastSeen = &l
|
||||
n.Online = &online
|
||||
}
|
||||
|
||||
var user = ToUserProfile(m.User)
|
||||
|
||||
@@ -3,31 +3,34 @@ package provider
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type OIDCProvider struct {
|
||||
clientID string
|
||||
clientSecret string
|
||||
scopes []string
|
||||
provider *oidc.Provider
|
||||
verifier *oidc.IDTokenVerifier
|
||||
}
|
||||
|
||||
func NewOIDCProvider(c *domain.AuthMethod) (*OIDCProvider, error) {
|
||||
func NewOIDCProvider(c *config.AuthProvider) (*OIDCProvider, error) {
|
||||
defaultScopes := []string{oidc.ScopeOpenID, "email", "profile"}
|
||||
provider, err := oidc.NewProvider(context.Background(), c.Issuer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
verifier := provider.Verifier(&oidc.Config{ClientID: c.ClientId})
|
||||
verifier := provider.Verifier(&oidc.Config{ClientID: c.ClientID, SkipClientIDCheck: c.ClientID == ""})
|
||||
|
||||
return &OIDCProvider{
|
||||
clientID: c.ClientId,
|
||||
clientID: c.ClientID,
|
||||
clientSecret: c.ClientSecret,
|
||||
scopes: append(defaultScopes, c.Scopes...),
|
||||
provider: provider,
|
||||
verifier: verifier,
|
||||
}, nil
|
||||
@@ -82,10 +85,14 @@ func (p *OIDCProvider) Exchange(redirectURI, code string) (*User, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
domain := strings.Split(email, "@")[1]
|
||||
|
||||
return &User{
|
||||
ID: sub,
|
||||
Name: email,
|
||||
Attr: map[string]interface{}{
|
||||
"email": email,
|
||||
"domain": domain,
|
||||
"token": tokenClaims,
|
||||
"userinfo": userInfoClaims,
|
||||
},
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
)
|
||||
|
||||
type AuthProvider interface {
|
||||
GetLoginURL(redirectURI, state string) string
|
||||
Exchange(redirectURI, code string) (*User, error)
|
||||
@@ -16,12 +10,3 @@ type User struct {
|
||||
Name string
|
||||
Attr map[string]interface{}
|
||||
}
|
||||
|
||||
func NewProvider(m *domain.AuthMethod) (AuthProvider, error) {
|
||||
switch m.Type {
|
||||
case "oidc":
|
||||
return NewOIDCProvider(m)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown auth method type")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/v2"
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery"
|
||||
"github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/jsiebens/ionscale/internal/key"
|
||||
"github.com/jsiebens/ionscale/internal/service"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
grpc_prometheus.EnableHandlingTimeHistogram()
|
||||
}
|
||||
|
||||
func NewGrpcServer(logger hclog.Logger, systemAdminKey key.ServerPrivate) *grpc.Server {
|
||||
return grpc.NewServer(
|
||||
middleware.WithUnaryServerChain(
|
||||
logging.UnaryServerInterceptor(
|
||||
&grpcLogger{logger.Named("grpc")},
|
||||
logging.WithDurationField(logging.DurationToDurationField),
|
||||
),
|
||||
grpc_prometheus.UnaryServerInterceptor,
|
||||
recovery.UnaryServerInterceptor(),
|
||||
service.UnaryServerTokenAuth(systemAdminKey),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
type grpcLogger struct {
|
||||
log hclog.Logger
|
||||
}
|
||||
|
||||
func (l *grpcLogger) Log(lvl logging.Level, msg string) {
|
||||
switch lvl {
|
||||
case logging.ERROR:
|
||||
l.log.Error(msg)
|
||||
default:
|
||||
l.log.Debug(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *grpcLogger) With(fields ...string) logging.Logger {
|
||||
if len(fields) == 0 {
|
||||
return l
|
||||
}
|
||||
vals := make([]interface{}, 0, len(fields))
|
||||
for i := 0; i < len(fields); i++ {
|
||||
vals = append(vals, fields[i])
|
||||
}
|
||||
return &grpcLogger{log: l.log.With(vals...)}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/key"
|
||||
"github.com/jsiebens/ionscale/internal/service"
|
||||
apiconnect "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1/ionscalev1connect"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func NewRpcHandler(systemAdminKey *key.ServerPrivate, repository domain.Repository, handler apiconnect.IonscaleServiceHandler) (string, http.Handler) {
|
||||
interceptors := connect.WithInterceptors(service.AuthenticationInterceptor(systemAdminKey, repository))
|
||||
return apiconnect.NewIonscaleServiceHandler(handler, interceptors)
|
||||
}
|
||||
+93
-62
@@ -6,79 +6,83 @@ import (
|
||||
"fmt"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/improbable-eng/grpc-web/go/grpcweb"
|
||||
"github.com/jsiebens/ionscale/internal/bind"
|
||||
"github.com/jsiebens/ionscale/internal/broker"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/database"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/handlers"
|
||||
"github.com/jsiebens/ionscale/internal/provider"
|
||||
"github.com/jsiebens/ionscale/internal/service"
|
||||
"github.com/jsiebens/ionscale/internal/templates"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
echo_prometheus "github.com/labstack/echo-contrib/prometheus"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/soheilhy/cmux"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func Start(config *config.Config) error {
|
||||
logger, err := setupLogging(config.Logging)
|
||||
func Start(c *config.Config) error {
|
||||
logger, err := setupLogging(c.Logging)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Starting ionscale server")
|
||||
|
||||
serverKey, err := config.ReadServerKeys()
|
||||
repository, brokers, err := database.OpenDB(&c.Database, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, repository, err := database.OpenDB(&config.Database, logger)
|
||||
defaultControlKeys, err := repository.GetControlKeys(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
controlKeys, err := repository.GetControlKeys(context.Background())
|
||||
serverKey, err := c.ReadServerKeys(defaultControlKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
brokers := broker.NewBrokerPool()
|
||||
offlineTimers := handlers.NewOfflineTimers(repository, brokers)
|
||||
reaper := handlers.NewReaper(brokers, repository)
|
||||
|
||||
go offlineTimers.Start()
|
||||
go reaper.Start()
|
||||
|
||||
serverUrl, err := url.Parse(c.ServerUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// prepare CertMagic
|
||||
if config.Tls.CertMagicDomain != "" {
|
||||
if c.Tls.AcmeEnabled {
|
||||
certmagic.DefaultACME.Agreed = true
|
||||
certmagic.DefaultACME.Email = config.Tls.CertMagicEmail
|
||||
certmagic.DefaultACME.CA = config.Tls.CertMagicCA
|
||||
if config.Tls.CertMagicStoragePath != "" {
|
||||
certmagic.Default.Storage = &certmagic.FileStorage{Path: config.Tls.CertMagicStoragePath}
|
||||
certmagic.DefaultACME.Email = c.Tls.AcmeEmail
|
||||
certmagic.DefaultACME.CA = c.Tls.AcmeCA
|
||||
if c.Tls.AcmePath != "" {
|
||||
certmagic.Default.Storage = &certmagic.FileStorage{Path: c.Tls.AcmePath}
|
||||
}
|
||||
|
||||
cfg := certmagic.NewDefault()
|
||||
if err := cfg.ManageAsync(context.Background(), []string{config.Tls.CertMagicDomain}); err != nil {
|
||||
if err := cfg.ManageAsync(context.Background(), []string{serverUrl.Host}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config.HttpListenAddr = fmt.Sprintf(":%d", certmagic.HTTPPort)
|
||||
config.HttpsListenAddr = fmt.Sprintf(":%d", certmagic.HTTPSPort)
|
||||
c.HttpListenAddr = fmt.Sprintf(":%d", certmagic.HTTPPort)
|
||||
c.HttpsListenAddr = fmt.Sprintf(":%d", certmagic.HTTPSPort)
|
||||
}
|
||||
|
||||
createPeerHandler := func(p key.MachinePublic) http.Handler {
|
||||
registrationHandlers := handlers.NewRegistrationHandlers(bind.DefaultBinder(p), config, brokers, repository)
|
||||
registrationHandlers := handlers.NewRegistrationHandlers(bind.DefaultBinder(p), c, brokers, repository)
|
||||
pollNetMapHandler := handlers.NewPollNetMapHandler(bind.DefaultBinder(p), brokers, repository, offlineTimers)
|
||||
|
||||
e := echo.New()
|
||||
@@ -90,14 +94,24 @@ func Start(config *config.Config) error {
|
||||
return e
|
||||
}
|
||||
|
||||
noiseHandlers := handlers.NewNoiseHandlers(controlKeys.ControlKey, createPeerHandler)
|
||||
registrationHandlers := handlers.NewRegistrationHandlers(bind.BoxBinder(controlKeys.LegacyControlKey), config, brokers, repository)
|
||||
pollNetMapHandler := handlers.NewPollNetMapHandler(bind.BoxBinder(controlKeys.LegacyControlKey), brokers, repository, offlineTimers)
|
||||
authProvider, systemIAMPolicy, err := setupAuthProvider(c.AuthProvider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error configuring OIDC provider: %v", err)
|
||||
}
|
||||
|
||||
noiseHandlers := handlers.NewNoiseHandlers(serverKey.ControlKey, createPeerHandler)
|
||||
registrationHandlers := handlers.NewRegistrationHandlers(bind.BoxBinder(serverKey.LegacyControlKey), c, brokers, repository)
|
||||
pollNetMapHandler := handlers.NewPollNetMapHandler(bind.BoxBinder(serverKey.LegacyControlKey), brokers, repository, offlineTimers)
|
||||
authenticationHandlers := handlers.NewAuthenticationHandlers(
|
||||
config,
|
||||
c,
|
||||
authProvider,
|
||||
systemIAMPolicy,
|
||||
repository,
|
||||
)
|
||||
|
||||
rpcService := service.NewService(c, authProvider, repository, brokers)
|
||||
rpcPath, rpcHandler := NewRpcHandler(serverKey.SystemAdminKey, repository, rpcService)
|
||||
|
||||
p := echo_prometheus.NewPrometheus("http", nil)
|
||||
|
||||
metricsHandler := echo.New()
|
||||
@@ -108,9 +122,10 @@ func Start(config *config.Config) error {
|
||||
nonTlsAppHandler.Use(EchoLogger(logger))
|
||||
nonTlsAppHandler.Use(p.HandlerFunc)
|
||||
nonTlsAppHandler.POST("/ts2021", noiseHandlers.Upgrade)
|
||||
nonTlsAppHandler.Any("/*", handlers.HttpRedirectHandler(config.Tls))
|
||||
nonTlsAppHandler.Any("/*", handlers.HttpRedirectHandler(c.Tls))
|
||||
|
||||
tlsAppHandler := echo.New()
|
||||
tlsAppHandler.Pre(handlers.HttpsRedirect(c.Tls))
|
||||
tlsAppHandler.Renderer = templates.NewTemplates()
|
||||
tlsAppHandler.Use(EchoRecover(logger))
|
||||
tlsAppHandler.Use(EchoLogger(logger))
|
||||
@@ -118,99 +133,115 @@ func Start(config *config.Config) error {
|
||||
|
||||
tlsAppHandler.Any("/*", handlers.IndexHandler(http.StatusNotFound))
|
||||
tlsAppHandler.Any("/", handlers.IndexHandler(http.StatusOK))
|
||||
tlsAppHandler.POST(rpcPath+"*", echo.WrapHandler(rpcHandler))
|
||||
tlsAppHandler.GET("/version", handlers.Version)
|
||||
tlsAppHandler.GET("/key", handlers.KeyHandler(controlKeys))
|
||||
tlsAppHandler.GET("/key", handlers.KeyHandler(serverKey))
|
||||
tlsAppHandler.POST("/ts2021", noiseHandlers.Upgrade)
|
||||
tlsAppHandler.POST("/machine/:id", registrationHandlers.Register)
|
||||
tlsAppHandler.POST("/machine/:id/map", pollNetMapHandler.PollNetMap)
|
||||
|
||||
auth := tlsAppHandler.Group("/a")
|
||||
auth.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||
TokenLookup: "form:_csrf",
|
||||
}))
|
||||
auth.GET("/:key", authenticationHandlers.StartAuth)
|
||||
auth.POST("/:key", authenticationHandlers.ProcessAuth)
|
||||
auth.GET("/c/:key", authenticationHandlers.StartCliAuth)
|
||||
auth.GET("/callback", authenticationHandlers.Callback)
|
||||
auth.POST("/callback", authenticationHandlers.EndOAuth)
|
||||
auth.GET("/success", authenticationHandlers.Success)
|
||||
auth.GET("/error", authenticationHandlers.Error)
|
||||
|
||||
grpcService := service.NewService(repository, brokers)
|
||||
grpcServer := NewGrpcServer(logger, serverKey.SystemAdminKey)
|
||||
api.RegisterIonscaleServer(grpcServer, grpcService)
|
||||
|
||||
tlsL, err := tlsListener(config)
|
||||
tlsL, err := tlsListener(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nonTlsL, err := nonTlsListener(config)
|
||||
nonTlsL, err := nonTlsListener(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metricsL, err := metricsListener(config)
|
||||
metricsL, err := metricsListener(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mux := cmux.New(selectListener(tlsL, nonTlsL))
|
||||
grpcL := mux.MatchWithWriters(
|
||||
cmux.HTTP2MatchHeaderFieldPrefixSendSettings("content-type", "application/grpc"),
|
||||
cmux.HTTP2MatchHeaderFieldPrefixSendSettings("content-type", "application/grpc+proto"),
|
||||
)
|
||||
grpcWebL := mux.Match(cmux.HTTP1HeaderFieldPrefix("content-type", "application/grpc-web"))
|
||||
httpL := mux.Match(cmux.Any())
|
||||
|
||||
grpcWebHandler := grpcweb.WrapServer(grpcServer)
|
||||
httpL := selectListener(tlsL, nonTlsL)
|
||||
http2Server := &http2.Server{}
|
||||
g := new(errgroup.Group)
|
||||
|
||||
g.Go(func() error { return grpcServer.Serve(grpcL) })
|
||||
g.Go(func() error { return http.Serve(grpcWebL, h2c.NewHandler(grpcWebHandler, http2Server)) })
|
||||
g.Go(func() error { return http.Serve(httpL, h2c.NewHandler(tlsAppHandler, http2Server)) })
|
||||
g.Go(func() error { return http.Serve(metricsL, metricsHandler) })
|
||||
g.Go(func() error { return mux.Serve() })
|
||||
|
||||
if tlsL != nil {
|
||||
g.Go(func() error { return http.Serve(nonTlsL, nonTlsAppHandler) })
|
||||
}
|
||||
|
||||
if config.Tls.CertMagicDomain != "" {
|
||||
logger.Info("TLS is enabled with CertMagic", "domain", config.Tls.CertMagicDomain)
|
||||
logger.Info("Server is running", "http_addr", config.HttpListenAddr, "https_addr", config.HttpsListenAddr, "metrics_addr", config.MetricsListenAddr)
|
||||
} else if !config.Tls.Disable {
|
||||
logger.Info("TLS is enabled", "cert", config.Tls.CertFile)
|
||||
logger.Info("Server is running", "http_addr", config.HttpListenAddr, "https_addr", config.HttpsListenAddr, "metrics_addr", config.MetricsListenAddr)
|
||||
if c.Tls.AcmeEnabled {
|
||||
logger.Info("TLS is enabled with ACME", "domain", serverUrl.Host)
|
||||
logger.Info("Server is running", "http_addr", c.HttpListenAddr, "https_addr", c.HttpsListenAddr, "metrics_addr", c.MetricsListenAddr)
|
||||
} else if !c.Tls.Disable {
|
||||
logger.Info("TLS is enabled", "cert", c.Tls.CertFile)
|
||||
logger.Info("Server is running", "http_addr", c.HttpListenAddr, "https_addr", c.HttpsListenAddr, "metrics_addr", c.MetricsListenAddr)
|
||||
} else {
|
||||
logger.Warn("TLS is disabled")
|
||||
logger.Info("Server is running", "http_addr", config.HttpListenAddr, "metrics_addr", config.MetricsListenAddr)
|
||||
logger.Info("Server is running", "http_addr", c.HttpListenAddr, "metrics_addr", c.MetricsListenAddr)
|
||||
}
|
||||
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func setupAuthProvider(config config.AuthProvider) (provider.AuthProvider, *domain.IAMPolicy, error) {
|
||||
if len(config.Issuer) == 0 {
|
||||
return nil, &domain.IAMPolicy{}, nil
|
||||
}
|
||||
|
||||
authProvider, err := provider.NewOIDCProvider(&config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return authProvider, &domain.IAMPolicy{
|
||||
Subs: config.SystemAdminPolicy.Subs,
|
||||
Emails: config.SystemAdminPolicy.Emails,
|
||||
Filters: config.SystemAdminPolicy.Filters,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func metricsListener(config *config.Config) (net.Listener, error) {
|
||||
return net.Listen("tcp", config.MetricsListenAddr)
|
||||
}
|
||||
|
||||
func tlsListener(config *config.Config) (net.Listener, error) {
|
||||
if config.Tls.CertMagicDomain != "" {
|
||||
if config.Tls.Disable {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if config.Tls.AcmeEnabled {
|
||||
cfg := certmagic.NewDefault()
|
||||
tlsConfig := cfg.TLSConfig()
|
||||
tlsConfig.NextProtos = append([]string{"h2", "http/1.1"}, tlsConfig.NextProtos...)
|
||||
return tls.Listen("tcp", config.HttpsListenAddr, tlsConfig)
|
||||
}
|
||||
|
||||
if !config.Tls.Disable {
|
||||
cer, err := tls.LoadX509KeyPair(config.Tls.CertFile, config.Tls.KeyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{Certificates: []tls.Certificate{cer}}
|
||||
|
||||
return tls.Listen("tcp", config.HttpsListenAddr, tlsConfig)
|
||||
certPEMBlock, err := os.ReadFile(config.Tls.CertFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading cert file: %v", err)
|
||||
}
|
||||
keyPEMBlock, err := os.ReadFile(config.Tls.KeyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading key file: %v", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
cer, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading cert and key file: %v", err)
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{Certificates: []tls.Certificate{cer}}
|
||||
|
||||
return tls.Listen("tcp", config.HttpsListenAddr, tlsConfig)
|
||||
}
|
||||
|
||||
func nonTlsListener(config *config.Config) (net.Listener, error) {
|
||||
|
||||
+39
-23
@@ -2,46 +2,62 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/broker"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"github.com/jsiebens/ionscale/internal/mapping"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
)
|
||||
|
||||
func (s *Service) GetACLPolicy(ctx context.Context, req *api.GetACLPolicyRequest) (*api.GetACLPolicyResponse, error) {
|
||||
policy, err := s.repository.GetACLPolicy(ctx, req.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (s *Service) GetACLPolicy(ctx context.Context, req *connect.Request[api.GetACLPolicyRequest]) (*connect.Response[api.GetACLPolicyResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
marshal, err := json.Marshal(policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &api.GetACLPolicyResponse{Value: marshal}, nil
|
||||
}
|
||||
|
||||
func (s *Service) SetACLPolicy(ctx context.Context, req *api.SetACLPolicyRequest) (*api.SetACLPolicyResponse, error) {
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.TailnetId)
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.Msg.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tailnet == nil {
|
||||
return nil, status.Error(codes.NotFound, "tailnet does not exist")
|
||||
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("tailnet does not exist"))
|
||||
}
|
||||
|
||||
var policy api.ACLPolicy
|
||||
if err := mapping.CopyViaJson(&tailnet.ACLPolicy, &policy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return connect.NewResponse(&api.GetACLPolicyResponse{Policy: &policy}), nil
|
||||
}
|
||||
|
||||
func (s *Service) SetACLPolicy(ctx context.Context, req *connect.Request[api.SetACLPolicyRequest]) (*connect.Response[api.SetACLPolicyResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.Msg.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tailnet == nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("tailnet does not exist"))
|
||||
}
|
||||
|
||||
var policy domain.ACLPolicy
|
||||
if err := json.Unmarshal(req.Value, &policy); err != nil {
|
||||
if err := mapping.CopyViaJson(req.Msg.Policy, &policy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.repository.SetACLPolicy(ctx, tailnet.ID, &policy); err != nil {
|
||||
tailnet.ACLPolicy = policy
|
||||
if err := s.repository.SaveTailnet(ctx, tailnet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.brokers(tailnet.ID).SignalACLUpdated()
|
||||
s.pubsub.Publish(tailnet.ID, &broker.Signal{ACLUpdated: true})
|
||||
|
||||
return &api.SetACLPolicyResponse{}, nil
|
||||
return connect.NewResponse(&api.SetACLPolicyResponse{}), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Service) Authenticate(ctx context.Context, req *connect.Request[api.AuthenticationRequest], stream *connect.ServerStream[api.AuthenticationResponse]) error {
|
||||
if s.authProvider == nil {
|
||||
return connect.NewError(connect.CodeFailedPrecondition, errors.New("no authentication method available, contact your ionscale administrator for more information"))
|
||||
}
|
||||
|
||||
key := util.RandStringBytes(8)
|
||||
authUrl := s.config.CreateUrl("/a/c/%s", key)
|
||||
|
||||
session := &domain.AuthenticationRequest{
|
||||
Key: key,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
if err := s.repository.SaveAuthenticationRequest(ctx, session); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := stream.Send(&api.AuthenticationResponse{AuthUrl: authUrl}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notify := ctx.Done()
|
||||
tick := time.NewTicker(1 * time.Second)
|
||||
|
||||
defer func() {
|
||||
tick.Stop()
|
||||
_ = s.repository.DeleteAuthenticationRequest(context.Background(), key)
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
m, err := s.repository.GetAuthenticationRequest(ctx, key)
|
||||
|
||||
if err != nil || m == nil {
|
||||
return connect.NewError(connect.CodeInternal, errors.New("something went wrong"))
|
||||
}
|
||||
|
||||
if len(m.Token) != 0 {
|
||||
if err := stream.Send(&api.AuthenticationResponse{Token: m.Token, TailnetId: m.TailnetID}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(m.Error) != 0 {
|
||||
return connect.NewError(connect.CodePermissionDenied, errors.New(m.Error))
|
||||
}
|
||||
|
||||
if err := stream.Send(&api.AuthenticationResponse{AuthUrl: authUrl}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case <-notify:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func (s *Service) ListAuthFilters(ctx context.Context, req *api.ListAuthFiltersRequest) (*api.ListAuthFiltersResponse, error) {
|
||||
response := &api.ListAuthFiltersResponse{AuthFilters: []*api.AuthFilter{}}
|
||||
|
||||
if req.AuthMethodId == nil {
|
||||
filters, err := s.repository.ListAuthFilters(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, filter := range filters {
|
||||
response.AuthFilters = append(response.AuthFilters, s.mapToApi(&filter.AuthMethod, filter))
|
||||
}
|
||||
} else {
|
||||
authMethod, err := s.repository.GetAuthMethod(ctx, *req.AuthMethodId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if authMethod == nil {
|
||||
return nil, status.Error(codes.NotFound, "invalid auth method id")
|
||||
}
|
||||
|
||||
filters, err := s.repository.ListAuthFiltersByAuthMethod(ctx, authMethod.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, filter := range filters {
|
||||
response.AuthFilters = append(response.AuthFilters, s.mapToApi(&filter.AuthMethod, filter))
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateAuthFilter(ctx context.Context, req *api.CreateAuthFilterRequest) (*api.CreateAuthFilterResponse, error) {
|
||||
authMethod, err := s.repository.GetAuthMethod(ctx, req.AuthMethodId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if authMethod == nil {
|
||||
return nil, status.Error(codes.NotFound, "invalid auth method id")
|
||||
}
|
||||
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tailnet == nil {
|
||||
return nil, status.Error(codes.NotFound, "invalid tailnet id")
|
||||
}
|
||||
|
||||
if req.Expr != "*" {
|
||||
if _, err := bexpr.CreateEvaluator(req.Expr); err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid expression: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
authFilter := &domain.AuthFilter{
|
||||
ID: util.NextID(),
|
||||
Expr: req.Expr,
|
||||
AuthMethod: *authMethod,
|
||||
Tailnet: tailnet,
|
||||
}
|
||||
|
||||
if err := s.repository.SaveAuthFilter(ctx, authFilter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := api.CreateAuthFilterResponse{AuthFilter: s.mapToApi(authMethod, *authFilter)}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteAuthFilter(ctx context.Context, req *api.DeleteAuthFilterRequest) (*api.DeleteAuthFilterResponse, error) {
|
||||
|
||||
err := s.repository.Transaction(func(rp domain.Repository) error {
|
||||
|
||||
filter, err := rp.GetAuthFilter(ctx, req.AuthFilterId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if filter == nil {
|
||||
return status.Error(codes.NotFound, "auth filter not found")
|
||||
}
|
||||
|
||||
c, err := rp.ExpireMachineByAuthMethod(ctx, filter.AuthMethodID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := rp.DeleteAuthFilter(ctx, filter.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c != 0 {
|
||||
s.brokers(*filter.TailnetID).SignalUpdate()
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := api.DeleteAuthFilterResponse{}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func (s *Service) mapToApi(authMethod *domain.AuthMethod, filter domain.AuthFilter) *api.AuthFilter {
|
||||
result := api.AuthFilter{
|
||||
Id: filter.ID,
|
||||
Expr: filter.Expr,
|
||||
AuthMethod: &api.Ref{
|
||||
Id: authMethod.ID,
|
||||
Name: authMethod.Name,
|
||||
},
|
||||
}
|
||||
|
||||
if filter.Tailnet != nil {
|
||||
id := filter.Tailnet.ID
|
||||
name := filter.Tailnet.Name
|
||||
|
||||
result.Tailnet = &api.Ref{
|
||||
Id: id,
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
+110
-44
@@ -2,22 +2,28 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Service) GetAuthKey(ctx context.Context, req *api.GetAuthKeyRequest) (*api.GetAuthKeyResponse, error) {
|
||||
key, err := s.repository.GetAuthKey(ctx, req.AuthKeyId)
|
||||
func (s *Service) GetAuthKey(ctx context.Context, req *connect.Request[api.GetAuthKeyRequest]) (*connect.Response[api.GetAuthKeyResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
|
||||
key, err := s.repository.GetAuthKey(ctx, req.Msg.AuthKeyId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if key == nil {
|
||||
return nil, status.Error(codes.NotFound, "")
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("auth key not found"))
|
||||
}
|
||||
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(key.TailnetID) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
var expiresAt *timestamppb.Timestamp
|
||||
@@ -25,7 +31,7 @@ func (s *Service) GetAuthKey(ctx context.Context, req *api.GetAuthKeyRequest) (*
|
||||
expiresAt = timestamppb.New(*key.ExpiresAt)
|
||||
}
|
||||
|
||||
return &api.GetAuthKeyResponse{AuthKey: &api.AuthKey{
|
||||
return connect.NewResponse(&api.GetAuthKeyResponse{AuthKey: &api.AuthKey{
|
||||
Id: key.ID,
|
||||
Key: key.Key,
|
||||
Ephemeral: key.Ephemeral,
|
||||
@@ -36,25 +42,11 @@ func (s *Service) GetAuthKey(ctx context.Context, req *api.GetAuthKeyRequest) (*
|
||||
Id: key.Tailnet.ID,
|
||||
Name: key.Tailnet.Name,
|
||||
},
|
||||
}}, nil
|
||||
}}), nil
|
||||
}
|
||||
|
||||
func (s *Service) ListAuthKeys(ctx context.Context, req *api.ListAuthKeysRequest) (*api.ListAuthKeysResponse, error) {
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tailnet == nil {
|
||||
return nil, status.Error(codes.NotFound, "")
|
||||
}
|
||||
|
||||
authKeys, err := s.repository.ListAuthKeys(ctx, req.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := api.ListAuthKeysResponse{}
|
||||
func mapAuthKeysToApi(authKeys []domain.AuthKey) []*api.AuthKey {
|
||||
var result []*api.AuthKey
|
||||
|
||||
for _, key := range authKeys {
|
||||
var expiresAt *timestamppb.Timestamp
|
||||
@@ -62,7 +54,7 @@ func (s *Service) ListAuthKeys(ctx context.Context, req *api.ListAuthKeysRequest
|
||||
expiresAt = timestamppb.New(*key.ExpiresAt)
|
||||
}
|
||||
|
||||
response.AuthKeys = append(response.AuthKeys, &api.AuthKey{
|
||||
result = append(result, &api.AuthKey{
|
||||
Id: key.ID,
|
||||
Key: key.Key,
|
||||
Ephemeral: key.Ephemeral,
|
||||
@@ -70,47 +62,106 @@ func (s *Service) ListAuthKeys(ctx context.Context, req *api.ListAuthKeysRequest
|
||||
CreatedAt: timestamppb.New(key.CreatedAt),
|
||||
ExpiresAt: expiresAt,
|
||||
Tailnet: &api.Ref{
|
||||
Id: tailnet.ID,
|
||||
Name: tailnet.Name,
|
||||
Id: key.Tailnet.ID,
|
||||
Name: key.Tailnet.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Service) CreateAuthKey(ctx context.Context, req *api.CreateAuthKeyRequest) (*api.CreateAuthKeyResponse, error) {
|
||||
if len(req.Tags) == 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "at least one tag is required when creating an auth key")
|
||||
func (s *Service) ListAuthKeys(ctx context.Context, req *connect.Request[api.ListAuthKeysRequest]) (*connect.Response[api.ListAuthKeysResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.TailnetId)
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.Msg.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tailnet == nil {
|
||||
return nil, status.Error(codes.NotFound, "")
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("tailnet not found"))
|
||||
}
|
||||
|
||||
response := api.ListAuthKeysResponse{}
|
||||
|
||||
if principal.IsSystemAdmin() {
|
||||
authKeys, err := s.repository.ListAuthKeys(ctx, req.Msg.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response.AuthKeys = mapAuthKeysToApi(authKeys)
|
||||
return connect.NewResponse(&response), nil
|
||||
}
|
||||
|
||||
if principal.User != nil {
|
||||
authKeys, err := s.repository.ListAuthKeysByTailnetAndUser(ctx, req.Msg.TailnetId, principal.User.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response.AuthKeys = mapAuthKeysToApi(authKeys)
|
||||
return connect.NewResponse(&response), nil
|
||||
}
|
||||
|
||||
return connect.NewResponse(&response), nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateAuthKey(ctx context.Context, req *connect.Request[api.CreateAuthKeyRequest]) (*connect.Response[api.CreateAuthKeyResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
if principal.User == nil && len(req.Msg.Tags) == 0 {
|
||||
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("at least one tag is required when creating an auth key"))
|
||||
}
|
||||
|
||||
if err := domain.CheckTags(req.Msg.Tags); err != nil {
|
||||
return nil, connect.NewError(connect.CodeInvalidArgument, err)
|
||||
}
|
||||
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.Msg.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tailnet == nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("tailnet not found"))
|
||||
}
|
||||
|
||||
if !principal.IsSystemAdmin() {
|
||||
if err := tailnet.ACLPolicy.CheckTagOwners(req.Msg.Tags, principal.User); err != nil {
|
||||
return nil, connect.NewError(connect.CodeInvalidArgument, err)
|
||||
}
|
||||
}
|
||||
|
||||
var expiresAt *time.Time
|
||||
var expiresAtPb *timestamppb.Timestamp
|
||||
|
||||
if req.Expiry != nil {
|
||||
duration := req.Expiry.AsDuration()
|
||||
if req.Msg.Expiry != nil {
|
||||
duration := req.Msg.Expiry.AsDuration()
|
||||
e := time.Now().UTC().Add(duration)
|
||||
expiresAt = &e
|
||||
expiresAtPb = timestamppb.New(*expiresAt)
|
||||
}
|
||||
|
||||
user, _, err := s.repository.GetOrCreateServiceUser(ctx, tailnet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var user = principal.User
|
||||
if user == nil {
|
||||
u, _, err := s.repository.GetOrCreateServiceUser(ctx, tailnet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user = u
|
||||
}
|
||||
|
||||
tags := domain.SanitizeTags(req.Tags)
|
||||
tags := domain.SanitizeTags(req.Msg.Tags)
|
||||
|
||||
v, authKey := domain.CreateAuthKey(tailnet, user, req.Ephemeral, tags, expiresAt)
|
||||
v, authKey := domain.CreateAuthKey(tailnet, user, req.Msg.Ephemeral, tags, expiresAt)
|
||||
|
||||
if err := s.repository.SaveAuthKey(ctx, authKey); err != nil {
|
||||
return nil, err
|
||||
@@ -131,12 +182,27 @@ func (s *Service) CreateAuthKey(ctx context.Context, req *api.CreateAuthKeyReque
|
||||
},
|
||||
}}
|
||||
|
||||
return &response, nil
|
||||
return connect.NewResponse(&response), nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteAuthKey(ctx context.Context, req *api.DeleteAuthKeyRequest) (*api.DeleteAuthKeyResponse, error) {
|
||||
if _, err := s.repository.DeleteAuthKey(ctx, req.AuthKeyId); err != nil {
|
||||
func (s *Service) DeleteAuthKey(ctx context.Context, req *connect.Request[api.DeleteAuthKeyRequest]) (*connect.Response[api.DeleteAuthKeyResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
|
||||
key, err := s.repository.GetAuthKey(ctx, req.Msg.AuthKeyId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &api.DeleteAuthKeyResponse{}, nil
|
||||
|
||||
if key == nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("auth key not found"))
|
||||
}
|
||||
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(key.UserID) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
if _, err := s.repository.DeleteAuthKey(ctx, req.Msg.AuthKeyId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return connect.NewResponse(&api.DeleteAuthKeyResponse{}), nil
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
)
|
||||
|
||||
func (s *Service) CreateAuthMethod(ctx context.Context, req *api.CreateAuthMethodRequest) (*api.CreateAuthMethodResponse, error) {
|
||||
|
||||
authMethod := &domain.AuthMethod{
|
||||
ID: util.NextID(),
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Issuer: req.Issuer,
|
||||
ClientId: req.ClientId,
|
||||
ClientSecret: req.ClientSecret,
|
||||
}
|
||||
|
||||
if err := s.repository.SaveAuthMethod(ctx, authMethod); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &api.CreateAuthMethodResponse{AuthMethod: &api.AuthMethod{
|
||||
Id: authMethod.ID,
|
||||
Type: authMethod.Type,
|
||||
Name: authMethod.Name,
|
||||
Issuer: authMethod.Issuer,
|
||||
ClientId: authMethod.ClientId,
|
||||
}}, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *Service) ListAuthMethods(ctx context.Context, _ *api.ListAuthMethodsRequest) (*api.ListAuthMethodsResponse, error) {
|
||||
methods, err := s.repository.ListAuthMethods(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &api.ListAuthMethodsResponse{AuthMethods: []*api.AuthMethod{}}
|
||||
for _, m := range methods {
|
||||
response.AuthMethods = append(response.AuthMethods, &api.AuthMethod{
|
||||
Id: m.ID,
|
||||
Name: m.Name,
|
||||
Type: m.Type,
|
||||
})
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
@@ -3,11 +3,19 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"errors"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/broker"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func (s *Service) GetDERPMap(ctx context.Context, req *api.GetDERPMapRequest) (*api.GetDERPMapResponse, error) {
|
||||
func (s *Service) GetDERPMap(ctx context.Context, _ *connect.Request[api.GetDERPMapRequest]) (*connect.Response[api.GetDERPMapResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
derpMap, err := s.repository.GetDERPMap(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -18,12 +26,22 @@ func (s *Service) GetDERPMap(ctx context.Context, req *api.GetDERPMapRequest) (*
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &api.GetDERPMapResponse{Value: raw}, nil
|
||||
return connect.NewResponse(&api.GetDERPMapResponse{Value: raw}), nil
|
||||
}
|
||||
|
||||
func (s *Service) SetDERPMap(ctx context.Context, req *api.SetDERPMapRequest) (*api.SetDERPMapResponse, error) {
|
||||
func (s *Service) SetDERPMap(ctx context.Context, req *connect.Request[api.SetDERPMapRequest]) (*connect.Response[api.SetDERPMapResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
var derpMap tailcfg.DERPMap
|
||||
err := json.Unmarshal(req.Value, &derpMap)
|
||||
err := json.Unmarshal(req.Msg.Value, &derpMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tailnets, err := s.repository.ListTailnets(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -32,7 +50,9 @@ func (s *Service) SetDERPMap(ctx context.Context, req *api.SetDERPMapRequest) (*
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.brokerPool.SignalDERPMapUpdated(&derpMap)
|
||||
for _, t := range tailnets {
|
||||
s.pubsub.Publish(t.ID, &broker.Signal{})
|
||||
}
|
||||
|
||||
return &api.SetDERPMapResponse{Value: req.Value}, nil
|
||||
return connect.NewResponse(&api.SetDERPMapResponse{Value: req.Msg.Value}), nil
|
||||
}
|
||||
|
||||
+38
-26
@@ -2,69 +2,81 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/broker"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
)
|
||||
|
||||
func (s *Service) GetDNSConfig(ctx context.Context, req *api.GetDNSConfigRequest) (*api.GetDNSConfigResponse, error) {
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.TailnetId)
|
||||
func (s *Service) GetDNSConfig(ctx context.Context, req *connect.Request[api.GetDNSConfigRequest]) (*connect.Response[api.GetDNSConfigResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.Msg.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tailnet == nil {
|
||||
return nil, status.Error(codes.NotFound, "tailnet does not exist")
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("tailnet not found"))
|
||||
}
|
||||
|
||||
config, err := s.repository.GetDNSConfig(ctx, tailnet.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dnsConfig := tailnet.DNSConfig
|
||||
tailnetDomain := domain.SanitizeTailnetName(tailnet.Name)
|
||||
|
||||
resp := &api.GetDNSConfigResponse{
|
||||
Config: &api.DNSConfig{
|
||||
MagicDns: config.MagicDNS,
|
||||
OverrideLocalDns: config.OverrideLocalDNS,
|
||||
Nameservers: config.Nameservers,
|
||||
Routes: domainRoutesToApiRoutes(config.Routes),
|
||||
MagicDns: dnsConfig.MagicDNS,
|
||||
MagicDnsSuffix: fmt.Sprintf("%s.%s", tailnetDomain, config.MagicDNSSuffix()),
|
||||
OverrideLocalDns: dnsConfig.OverrideLocalDNS,
|
||||
Nameservers: dnsConfig.Nameservers,
|
||||
Routes: domainRoutesToApiRoutes(dnsConfig.Routes),
|
||||
},
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
return connect.NewResponse(resp), nil
|
||||
}
|
||||
|
||||
func (s *Service) SetDNSConfig(ctx context.Context, req *api.SetDNSConfigRequest) (*api.SetDNSConfigResponse, error) {
|
||||
dnsConfig := req.Config
|
||||
|
||||
if dnsConfig.MagicDns && len(dnsConfig.Nameservers) == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "at least one global nameserver is required when enabling magic dns")
|
||||
func (s *Service) SetDNSConfig(ctx context.Context, req *connect.Request[api.SetDNSConfigRequest]) (*connect.Response[api.SetDNSConfigResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.TailnetId)
|
||||
dnsConfig := req.Msg.Config
|
||||
|
||||
if dnsConfig.MagicDns && len(dnsConfig.Nameservers) == 0 {
|
||||
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("at least one global nameserver is required when enabling magic dns"))
|
||||
}
|
||||
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.Msg.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tailnet == nil {
|
||||
return nil, status.Error(codes.NotFound, "tailnet does not exist")
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("tailnet not found"))
|
||||
}
|
||||
|
||||
config := domain.DNSConfig{
|
||||
tailnet.DNSConfig = domain.DNSConfig{
|
||||
MagicDNS: dnsConfig.MagicDns,
|
||||
OverrideLocalDNS: dnsConfig.OverrideLocalDns,
|
||||
Nameservers: dnsConfig.Nameservers,
|
||||
Routes: apiRoutesToDomainRoutes(dnsConfig.Routes),
|
||||
}
|
||||
|
||||
if err := s.repository.SetDNSConfig(ctx, tailnet.ID, &config); err != nil {
|
||||
if err := s.repository.SaveTailnet(ctx, tailnet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.brokers(tailnet.ID).SignalDNSUpdated()
|
||||
s.pubsub.Publish(tailnet.ID, &broker.Signal{DNSUpdated: true})
|
||||
|
||||
resp := &api.SetDNSConfigResponse{Config: dnsConfig}
|
||||
|
||||
return resp, nil
|
||||
return connect.NewResponse(resp), nil
|
||||
}
|
||||
|
||||
func domainRoutesToApiRoutes(routes map[string][]string) map[string]*api.Routes {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
)
|
||||
|
||||
func (s *Service) GetIAMPolicy(ctx context.Context, req *connect.Request[api.GetIAMPolicyRequest]) (*connect.Response[api.GetIAMPolicyResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.Msg.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tailnet == nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("tailnet does not exist"))
|
||||
}
|
||||
|
||||
policy := &api.IAMPolicy{
|
||||
Subs: tailnet.IAMPolicy.Subs,
|
||||
Emails: tailnet.IAMPolicy.Emails,
|
||||
Filters: tailnet.IAMPolicy.Filters,
|
||||
Roles: domainRolesMapToApiRolesMap(tailnet.IAMPolicy.Roles),
|
||||
}
|
||||
|
||||
return connect.NewResponse(&api.GetIAMPolicyResponse{Policy: policy}), nil
|
||||
}
|
||||
|
||||
func (s *Service) SetIAMPolicy(ctx context.Context, req *connect.Request[api.SetIAMPolicyRequest]) (*connect.Response[api.SetIAMPolicyResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.Msg.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tailnet == nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("tailnet does not exist"))
|
||||
}
|
||||
|
||||
tailnet.IAMPolicy = domain.IAMPolicy{
|
||||
Subs: req.Msg.Policy.Subs,
|
||||
Emails: req.Msg.Policy.Emails,
|
||||
Filters: req.Msg.Policy.Filters,
|
||||
Roles: apiRolesMapToDomainRolesMap(req.Msg.Policy.Roles),
|
||||
}
|
||||
|
||||
if err := s.repository.SaveTailnet(ctx, tailnet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return connect.NewResponse(&api.SetIAMPolicyResponse{}), nil
|
||||
}
|
||||
|
||||
func apiRolesMapToDomainRolesMap(values map[string]string) map[string]domain.UserRole {
|
||||
var result = map[string]domain.UserRole{}
|
||||
for k, v := range values {
|
||||
result[k] = domain.UserRole(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func domainRolesMapToApiRolesMap(values map[string]domain.UserRole) map[string]string {
|
||||
var result = map[string]string{}
|
||||
for k, v := range values {
|
||||
result[k] = string(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/key"
|
||||
"github.com/jsiebens/ionscale/internal/token"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidToken = connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("invalid token"))
|
||||
)
|
||||
|
||||
const (
|
||||
principalKey = "principalKay"
|
||||
)
|
||||
|
||||
func CurrentPrincipal(ctx context.Context) domain.Principal {
|
||||
p := ctx.Value(principalKey)
|
||||
if p == nil {
|
||||
return domain.Principal{SystemRole: domain.SystemRoleNone, UserRole: domain.UserRoleNone}
|
||||
}
|
||||
return p.(domain.Principal)
|
||||
}
|
||||
|
||||
func AuthenticationInterceptor(systemAdminKey *key.ServerPrivate, repository domain.Repository) connect.UnaryInterceptorFunc {
|
||||
return func(next connect.UnaryFunc) connect.UnaryFunc {
|
||||
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
||||
name := req.Spec().Procedure
|
||||
|
||||
if strings.HasSuffix(name, "/GetVersion") {
|
||||
return next(ctx, req)
|
||||
}
|
||||
|
||||
authorizationHeader := req.Header().Get("Authorization")
|
||||
bearerToken := strings.TrimPrefix(authorizationHeader, "Bearer ")
|
||||
|
||||
if principal := exchangeToken(ctx, systemAdminKey, repository, bearerToken); principal != nil {
|
||||
return next(context.WithValue(ctx, principalKey, *principal), req)
|
||||
}
|
||||
|
||||
return nil, errInvalidToken
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func exchangeToken(ctx context.Context, systemAdminKey *key.ServerPrivate, repository domain.Repository, value string) *domain.Principal {
|
||||
if len(value) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if systemAdminKey != nil && token.IsSystemAdminToken(value) {
|
||||
_, err := token.ParseSystemAdminToken(*systemAdminKey, value)
|
||||
if err == nil {
|
||||
return &domain.Principal{SystemRole: domain.SystemRoleAdmin}
|
||||
}
|
||||
}
|
||||
|
||||
apiKey, err := repository.LoadApiKey(ctx, value)
|
||||
if err == nil && apiKey != nil {
|
||||
user := apiKey.User
|
||||
tailnet := apiKey.Tailnet
|
||||
role := tailnet.IAMPolicy.GetRole(user)
|
||||
|
||||
return &domain.Principal{User: &apiKey.User, SystemRole: domain.SystemRoleNone, UserRole: role}
|
||||
}
|
||||
|
||||
systemApiKey, err := repository.LoadSystemApiKey(ctx, value)
|
||||
if err == nil && systemApiKey != nil {
|
||||
return &domain.Principal{SystemRole: domain.SystemRoleAdmin}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+299
-97
@@ -2,22 +2,76 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/broker"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"inet.af/netaddr"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Service) ListMachines(ctx context.Context, req *api.ListMachinesRequest) (*api.ListMachinesResponse, error) {
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.TailnetId)
|
||||
func (s *Service) machineToApi(m *domain.Machine) *api.Machine {
|
||||
var lastSeen *timestamppb.Timestamp
|
||||
|
||||
var name = m.Name
|
||||
if m.NameIdx != 0 {
|
||||
name = fmt.Sprintf("%s-%d", m.Name, m.NameIdx)
|
||||
}
|
||||
|
||||
online := false
|
||||
if m.LastSeen != nil {
|
||||
lastSeen = timestamppb.New(*m.LastSeen)
|
||||
online = m.LastSeen.After(time.Now().Add(-config.KeepAliveInterval()))
|
||||
}
|
||||
|
||||
return &api.Machine{
|
||||
Id: m.ID,
|
||||
Name: name,
|
||||
Ipv4: m.IPv4.String(),
|
||||
Ipv6: m.IPv6.String(),
|
||||
Ephemeral: m.Ephemeral,
|
||||
Tags: m.Tags,
|
||||
LastSeen: lastSeen,
|
||||
CreatedAt: timestamppb.New(m.CreatedAt),
|
||||
ExpiresAt: timestamppb.New(m.ExpiresAt),
|
||||
KeyExpiryDisabled: m.KeyExpiryDisabled,
|
||||
Connected: online,
|
||||
Os: m.HostInfo.OS,
|
||||
ClientVersion: m.HostInfo.IPNVersion,
|
||||
Tailnet: &api.Ref{
|
||||
Id: m.Tailnet.ID,
|
||||
Name: m.Tailnet.Name,
|
||||
},
|
||||
User: &api.Ref{
|
||||
Id: m.User.ID,
|
||||
Name: m.User.Name,
|
||||
},
|
||||
ClientConnectivity: &api.ClientConnectivity{
|
||||
Endpoints: m.Endpoints,
|
||||
},
|
||||
AdvertisedRoutes: m.AdvertisedPrefixes(),
|
||||
EnabledRoutes: m.AllowedPrefixes(),
|
||||
AdvertisedExitNode: m.IsAdvertisedExitNode(),
|
||||
EnabledExitNode: m.IsAllowedExitNode(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) ListMachines(ctx context.Context, req *connect.Request[api.ListMachinesRequest]) (*connect.Response[api.ListMachinesResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.Msg.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tailnet == nil {
|
||||
return nil, status.Error(codes.NotFound, "tailnet does not exist")
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("tailnet not found"))
|
||||
}
|
||||
|
||||
machines, err := s.repository.ListMachineByTailnet(ctx, tailnet.ID)
|
||||
@@ -27,150 +81,298 @@ func (s *Service) ListMachines(ctx context.Context, req *api.ListMachinesRequest
|
||||
|
||||
response := &api.ListMachinesResponse{}
|
||||
for _, m := range machines {
|
||||
var name = m.Name
|
||||
if m.NameIdx != 0 {
|
||||
name = fmt.Sprintf("%s-%d", m.Name, m.NameIdx)
|
||||
}
|
||||
online := s.brokers(m.TailnetID).IsConnected(m.ID)
|
||||
var lastSeen *timestamppb.Timestamp
|
||||
if m.LastSeen != nil {
|
||||
lastSeen = timestamppb.New(*m.LastSeen)
|
||||
}
|
||||
response.Machines = append(response.Machines, &api.Machine{
|
||||
Id: m.ID,
|
||||
Name: name,
|
||||
Ipv4: m.IPv4.String(),
|
||||
Ipv6: m.IPv6.String(),
|
||||
Ephemeral: m.Ephemeral,
|
||||
Tags: m.Tags,
|
||||
LastSeen: lastSeen,
|
||||
Connected: online,
|
||||
Tailnet: &api.Ref{
|
||||
Id: m.Tailnet.ID,
|
||||
Name: m.Tailnet.Name,
|
||||
},
|
||||
User: &api.Ref{
|
||||
Id: m.User.ID,
|
||||
Name: m.User.Name,
|
||||
},
|
||||
})
|
||||
response.Machines = append(response.Machines, s.machineToApi(&m))
|
||||
}
|
||||
|
||||
return response, nil
|
||||
return connect.NewResponse(response), nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteMachine(ctx context.Context, req *api.DeleteMachineRequest) (*api.DeleteMachineResponse, error) {
|
||||
m, err := s.repository.GetMachine(ctx, req.MachineId)
|
||||
func (s *Service) GetMachine(ctx context.Context, req *connect.Request[api.GetMachineRequest]) (*connect.Response[api.GetMachineResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
|
||||
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return nil, status.Error(codes.NotFound, "machine does not exist")
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("machine not found"))
|
||||
}
|
||||
|
||||
if _, err := s.repository.DeleteMachine(ctx, req.MachineId); err != nil {
|
||||
return nil, err
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
s.brokers(m.TailnetID).SignalPeersRemoved([]uint64{m.ID})
|
||||
|
||||
return &api.DeleteMachineResponse{}, nil
|
||||
return connect.NewResponse(&api.GetMachineResponse{Machine: s.machineToApi(m)}), nil
|
||||
}
|
||||
|
||||
func (s *Service) ExpireMachine(ctx context.Context, req *api.ExpireMachineRequest) (*api.ExpireMachineResponse, error) {
|
||||
m, err := s.repository.GetMachine(ctx, req.MachineId)
|
||||
func (s *Service) DeleteMachine(ctx context.Context, req *connect.Request[api.DeleteMachineRequest]) (*connect.Response[api.DeleteMachineResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
|
||||
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return nil, status.Error(codes.NotFound, "machine does not exist")
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("machine not found"))
|
||||
}
|
||||
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
if _, err := s.repository.DeleteMachine(ctx, req.Msg.MachineId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.pubsub.Publish(m.TailnetID, &broker.Signal{PeersRemoved: []uint64{m.ID}})
|
||||
|
||||
return connect.NewResponse(&api.DeleteMachineResponse{}), nil
|
||||
}
|
||||
|
||||
func (s *Service) ExpireMachine(ctx context.Context, req *connect.Request[api.ExpireMachineRequest]) (*connect.Response[api.ExpireMachineResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
|
||||
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("machine not found"))
|
||||
}
|
||||
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
timestamp := time.Unix(123, 0)
|
||||
m.ExpiresAt = ×tamp
|
||||
m.ExpiresAt = timestamp
|
||||
m.KeyExpiryDisabled = false
|
||||
|
||||
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.brokers(m.TailnetID).SignalPeerUpdated(m.ID)
|
||||
s.pubsub.Publish(m.TailnetID, &broker.Signal{PeerUpdated: &m.ID})
|
||||
|
||||
return &api.ExpireMachineResponse{}, nil
|
||||
return connect.NewResponse(&api.ExpireMachineResponse{}), nil
|
||||
}
|
||||
|
||||
func (s *Service) GetMachineRoutes(ctx context.Context, req *api.GetMachineRoutesRequest) (*api.GetMachineRoutesResponse, error) {
|
||||
|
||||
m, err := s.repository.GetMachine(ctx, req.MachineId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return nil, status.Error(codes.NotFound, "machine does not exist")
|
||||
}
|
||||
|
||||
var routes []*api.RoutableIP
|
||||
for _, r := range m.HostInfo.RoutableIPs {
|
||||
routes = append(routes, &api.RoutableIP{
|
||||
Advertised: r.String(),
|
||||
Allowed: m.IsAllowedIPPrefix(r),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) createMachineRoutesResponse(m *domain.Machine) (*connect.Response[api.GetMachineRoutesResponse], error) {
|
||||
response := api.GetMachineRoutesResponse{
|
||||
Routes: routes,
|
||||
AdvertisedRoutes: m.AdvertisedPrefixes(),
|
||||
EnabledRoutes: m.AllowedPrefixes(),
|
||||
AdvertisedExitNode: m.IsAdvertisedExitNode(),
|
||||
EnabledExitNode: m.IsAllowedExitNode(),
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
return connect.NewResponse(&response), nil
|
||||
}
|
||||
|
||||
func (s *Service) SetMachineRoutes(ctx context.Context, req *api.SetMachineRoutesRequest) (*api.GetMachineRoutesResponse, error) {
|
||||
m, err := s.repository.GetMachine(ctx, req.MachineId)
|
||||
func (s *Service) GetMachineRoutes(ctx context.Context, req *connect.Request[api.GetMachineRoutesRequest]) (*connect.Response[api.GetMachineRoutesResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
|
||||
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return nil, status.Error(codes.NotFound, "machine does not exist")
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("machine not found"))
|
||||
}
|
||||
|
||||
var allowedIps []netaddr.IPPrefix
|
||||
for _, r := range req.AllowedIps {
|
||||
prefix, err := netaddr.ParseIPPrefix(r)
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
return s.createMachineRoutesResponse(m)
|
||||
}
|
||||
|
||||
func (s *Service) EnableMachineRoutes(ctx context.Context, req *connect.Request[api.EnableMachineRoutesRequest]) (*connect.Response[api.GetMachineRoutesResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
|
||||
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("machine not found"))
|
||||
}
|
||||
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
var allowIPs = domain.NewAllowIPsSet(m.AllowIPs)
|
||||
var autoAllowIPs = domain.NewAllowIPsSet(m.AutoAllowIPs)
|
||||
|
||||
if req.Msg.Replace {
|
||||
allowIPs = domain.NewAllowIPsSet([]netip.Prefix{})
|
||||
autoAllowIPs = domain.NewAllowIPsSet([]netip.Prefix{})
|
||||
}
|
||||
|
||||
for _, r := range req.Msg.Routes {
|
||||
prefix, err := netip.ParsePrefix(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowedIps = append(allowedIps, prefix)
|
||||
allowIPs.Add(prefix)
|
||||
}
|
||||
|
||||
m.AllowIPs = allowedIps
|
||||
m.AllowIPs = allowIPs.Items()
|
||||
m.AutoAllowIPs = autoAllowIPs.Items()
|
||||
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.brokers(m.TailnetID).SignalPeerUpdated(m.ID)
|
||||
s.pubsub.Publish(m.TailnetID, &broker.Signal{PeerUpdated: &m.ID})
|
||||
|
||||
var routes []*api.RoutableIP
|
||||
for _, r := range m.HostInfo.RoutableIPs {
|
||||
routes = append(routes, &api.RoutableIP{
|
||||
Advertised: r.String(),
|
||||
Allowed: m.IsAllowedIPPrefix(r),
|
||||
})
|
||||
}
|
||||
|
||||
response := api.GetMachineRoutesResponse{
|
||||
Routes: routes,
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
return s.createMachineRoutesResponse(m)
|
||||
}
|
||||
|
||||
func mapIp(ip []netaddr.IPPrefix) []string {
|
||||
var x = []string{}
|
||||
for _, i := range ip {
|
||||
x = append(x, i.String())
|
||||
func (s *Service) DisableMachineRoutes(ctx context.Context, req *connect.Request[api.DisableMachineRoutesRequest]) (*connect.Response[api.GetMachineRoutesResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
|
||||
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x
|
||||
|
||||
if m == nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("machine not found"))
|
||||
}
|
||||
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
allowIPs := domain.NewAllowIPsSet(m.AllowIPs)
|
||||
autoAllowIPs := domain.NewAllowIPsSet(m.AutoAllowIPs)
|
||||
|
||||
for _, r := range req.Msg.Routes {
|
||||
prefix, err := netip.ParsePrefix(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowIPs.Remove(prefix)
|
||||
autoAllowIPs.Remove(prefix)
|
||||
}
|
||||
|
||||
m.AllowIPs = allowIPs.Items()
|
||||
m.AutoAllowIPs = autoAllowIPs.Items()
|
||||
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.pubsub.Publish(m.TailnetID, &broker.Signal{PeerUpdated: &m.ID})
|
||||
|
||||
return s.createMachineRoutesResponse(m)
|
||||
}
|
||||
|
||||
func (s *Service) EnableExitNode(ctx context.Context, req *connect.Request[api.EnableExitNodeRequest]) (*connect.Response[api.GetMachineRoutesResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
|
||||
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("machine not found"))
|
||||
}
|
||||
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
if !m.IsAdvertisedExitNode() {
|
||||
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("machine is not a valid exit node"))
|
||||
}
|
||||
|
||||
prefix4 := netip.MustParsePrefix("0.0.0.0/0")
|
||||
prefix6 := netip.MustParsePrefix("::/0")
|
||||
|
||||
allowIPs := domain.NewAllowIPsSet(m.AllowIPs)
|
||||
allowIPs.Add(prefix4, prefix6)
|
||||
|
||||
m.AllowIPs = allowIPs.Items()
|
||||
|
||||
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.pubsub.Publish(m.TailnetID, &broker.Signal{PeerUpdated: &m.ID})
|
||||
|
||||
return s.createMachineRoutesResponse(m)
|
||||
}
|
||||
|
||||
func (s *Service) DisableExitNode(ctx context.Context, req *connect.Request[api.DisableExitNodeRequest]) (*connect.Response[api.GetMachineRoutesResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
|
||||
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("machine not found"))
|
||||
}
|
||||
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
if !m.IsAdvertisedExitNode() {
|
||||
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("machine is not a valid exit node"))
|
||||
}
|
||||
|
||||
prefix4 := netip.MustParsePrefix("0.0.0.0/0")
|
||||
prefix6 := netip.MustParsePrefix("::/0")
|
||||
|
||||
allowIPs := domain.NewAllowIPsSet(m.AllowIPs)
|
||||
allowIPs.Remove(prefix4, prefix6)
|
||||
|
||||
autoAllowIPs := domain.NewAllowIPsSet(m.AutoAllowIPs)
|
||||
autoAllowIPs.Remove(prefix4, prefix6)
|
||||
|
||||
m.AllowIPs = allowIPs.Items()
|
||||
m.AutoAllowIPs = autoAllowIPs.Items()
|
||||
|
||||
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.pubsub.Publish(m.TailnetID, &broker.Signal{PeerUpdated: &m.ID})
|
||||
|
||||
return s.createMachineRoutesResponse(m)
|
||||
}
|
||||
|
||||
func (s *Service) SetMachineKeyExpiry(ctx context.Context, req *connect.Request[api.SetMachineKeyExpiryRequest]) (*connect.Response[api.SetMachineKeyExpiryResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
|
||||
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("machine not found"))
|
||||
}
|
||||
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
m.KeyExpiryDisabled = req.Msg.Disabled
|
||||
|
||||
if err := s.repository.SaveMachine(ctx, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.pubsub.Publish(m.TailnetID, &broker.Signal{PeerUpdated: &m.ID})
|
||||
|
||||
return connect.NewResponse(&api.SetMachineKeyExpiryResponse{}), nil
|
||||
}
|
||||
|
||||
+16
-64
@@ -2,83 +2,35 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/broker"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/key"
|
||||
"github.com/jsiebens/ionscale/internal/token"
|
||||
"github.com/jsiebens/ionscale/internal/provider"
|
||||
"github.com/jsiebens/ionscale/internal/version"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"strings"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
)
|
||||
|
||||
var (
|
||||
errMissingMetadata = status.Error(codes.InvalidArgument, "missing metadata")
|
||||
errInvalidToken = status.Error(codes.Unauthenticated, "invalid token")
|
||||
)
|
||||
|
||||
func NewService(repository domain.Repository, brokerPool *broker.BrokerPool) *Service {
|
||||
func NewService(config *config.Config, authProvider provider.AuthProvider, repository domain.Repository, pubsub broker.Pubsub) *Service {
|
||||
return &Service{
|
||||
repository: repository,
|
||||
brokerPool: brokerPool,
|
||||
config: config,
|
||||
authProvider: authProvider,
|
||||
repository: repository,
|
||||
pubsub: pubsub,
|
||||
}
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
repository domain.Repository
|
||||
brokerPool *broker.BrokerPool
|
||||
config *config.Config
|
||||
authProvider provider.AuthProvider
|
||||
repository domain.Repository
|
||||
pubsub broker.Pubsub
|
||||
}
|
||||
|
||||
func (s *Service) brokers(tailnetID uint64) broker.Broker {
|
||||
return s.brokerPool.Get(tailnetID)
|
||||
}
|
||||
|
||||
func (s *Service) GetVersion(ctx context.Context, req *api.GetVersionRequest) (*api.GetVersionResponse, error) {
|
||||
func (s *Service) GetVersion(_ context.Context, _ *connect.Request[api.GetVersionRequest]) (*connect.Response[api.GetVersionResponse], error) {
|
||||
v, revision := version.GetReleaseInfo()
|
||||
return &api.GetVersionResponse{
|
||||
return connect.NewResponse(&api.GetVersionResponse{
|
||||
Version: v,
|
||||
Revision: revision,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func UnaryServerTokenAuth(systemAdminKey key.ServerPrivate) func(context.Context, interface{}, *grpc.UnaryServerInfo, grpc.UnaryHandler) (interface{}, error) {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
|
||||
if strings.HasSuffix(info.FullMethod, "/GetVersion") {
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return nil, errMissingMetadata
|
||||
}
|
||||
|
||||
// The keys within metadata.MD are normalized to lowercase.
|
||||
// See: https://godoc.org/google.golang.org/grpc/metadata#New
|
||||
valid := validateAuthorizationToken(systemAdminKey, md["authorization"])
|
||||
|
||||
if valid {
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
return nil, errInvalidToken
|
||||
}
|
||||
}
|
||||
|
||||
func validateAuthorizationToken(systemAdminKey key.ServerPrivate, authorization []string) bool {
|
||||
if len(authorization) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
bearerToken := strings.TrimPrefix(authorization[0], "Bearer ")
|
||||
|
||||
if token.IsSystemAdminToken(bearerToken) {
|
||||
_, err := token.ParseSystemAdminToken(systemAdminKey, bearerToken)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
return false
|
||||
}), nil
|
||||
}
|
||||
|
||||
+67
-38
@@ -2,21 +2,35 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/broker"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
)
|
||||
|
||||
func (s *Service) CreateTailnet(ctx context.Context, req *api.CreateTailnetRequest) (*api.CreateTailnetResponse, error) {
|
||||
tailnet, created, err := s.repository.GetOrCreateTailnet(ctx, req.Name)
|
||||
func (s *Service) CreateTailnet(ctx context.Context, req *connect.Request[api.CreateTailnetRequest]) (*connect.Response[api.CreateTailnetResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
name := req.Msg.Name
|
||||
iamPolicy := domain.IAMPolicy{
|
||||
Subs: req.Msg.IamPolicy.Subs,
|
||||
Emails: req.Msg.IamPolicy.Emails,
|
||||
Filters: req.Msg.IamPolicy.Filters,
|
||||
Roles: apiRolesMapToDomainRolesMap(req.Msg.IamPolicy.Roles),
|
||||
}
|
||||
|
||||
tailnet, created, err := s.repository.GetOrCreateTailnet(ctx, name, iamPolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !created {
|
||||
return nil, fmt.Errorf("tailnet already exists")
|
||||
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("tailnet already exists"))
|
||||
}
|
||||
|
||||
resp := &api.CreateTailnetResponse{Tailnet: &api.Tailnet{
|
||||
@@ -24,76 +38,91 @@ func (s *Service) CreateTailnet(ctx context.Context, req *api.CreateTailnetReque
|
||||
Name: tailnet.Name,
|
||||
}}
|
||||
|
||||
return resp, nil
|
||||
return connect.NewResponse(resp), nil
|
||||
}
|
||||
|
||||
func (s *Service) GetTailnet(ctx context.Context, req *api.GetTailnetRequest) (*api.GetTailnetResponse, error) {
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.Id)
|
||||
func (s *Service) GetTailnet(ctx context.Context, req *connect.Request[api.GetTailnetRequest]) (*connect.Response[api.GetTailnetResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.Id) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.Msg.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tailnet == nil {
|
||||
return nil, status.Error(codes.NotFound, "")
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("tailnet not found"))
|
||||
}
|
||||
|
||||
return &api.GetTailnetResponse{Tailnet: &api.Tailnet{
|
||||
return connect.NewResponse(&api.GetTailnetResponse{Tailnet: &api.Tailnet{
|
||||
Id: tailnet.ID,
|
||||
Name: tailnet.Name,
|
||||
}}, nil
|
||||
}}), nil
|
||||
}
|
||||
|
||||
func (s *Service) ListTailnets(ctx context.Context, _ *api.ListTailnetRequest) (*api.ListTailnetResponse, error) {
|
||||
func (s *Service) ListTailnets(ctx context.Context, req *connect.Request[api.ListTailnetRequest]) (*connect.Response[api.ListTailnetResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
|
||||
resp := &api.ListTailnetResponse{}
|
||||
|
||||
tailnets, err := s.repository.ListTailnets(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if principal.IsSystemAdmin() {
|
||||
tailnets, err := s.repository.ListTailnets(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range tailnets {
|
||||
gt := api.Tailnet{Id: t.ID, Name: t.Name}
|
||||
resp.Tailnet = append(resp.Tailnet, >)
|
||||
}
|
||||
}
|
||||
for _, t := range tailnets {
|
||||
gt := api.Tailnet{Id: t.ID, Name: t.Name}
|
||||
|
||||
if principal.User != nil {
|
||||
tailnet, err := s.repository.GetTailnet(ctx, principal.User.TailnetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gt := api.Tailnet{Id: tailnet.ID, Name: tailnet.Name}
|
||||
resp.Tailnet = append(resp.Tailnet, >)
|
||||
}
|
||||
return resp, nil
|
||||
|
||||
return connect.NewResponse(resp), nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteTailnet(ctx context.Context, req *api.DeleteTailnetRequest) (*api.DeleteTailnetResponse, error) {
|
||||
func (s *Service) DeleteTailnet(ctx context.Context, req *connect.Request[api.DeleteTailnetRequest]) (*connect.Response[api.DeleteTailnetResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
count, err := s.repository.CountMachineByTailnet(ctx, req.TailnetId)
|
||||
count, err := s.repository.CountMachineByTailnet(ctx, req.Msg.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !req.Force && count > 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("tailnet is not empty, number of machines: %d", count))
|
||||
if !req.Msg.Force && count > 0 {
|
||||
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("tailnet is not empty, number of machines: %d", count))
|
||||
}
|
||||
|
||||
err = s.repository.Transaction(func(tx domain.Repository) error {
|
||||
if err := tx.DeleteMachineByTailnet(ctx, req.TailnetId); err != nil {
|
||||
if err := tx.DeleteMachineByTailnet(ctx, req.Msg.TailnetId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.DeleteAuthKeysByTailnet(ctx, req.TailnetId); err != nil {
|
||||
if err := tx.DeleteApiKeysByTailnet(ctx, req.Msg.TailnetId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.DeleteUsersByTailnet(ctx, req.TailnetId); err != nil {
|
||||
if err := tx.DeleteAuthKeysByTailnet(ctx, req.Msg.TailnetId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.DeleteAuthFiltersByTailnet(ctx, req.TailnetId); err != nil {
|
||||
if err := tx.DeleteUsersByTailnet(ctx, req.Msg.TailnetId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.DeleteACLPolicy(ctx, req.TailnetId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.DeleteDNSConfig(ctx, req.TailnetId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.DeleteTailnet(ctx, req.TailnetId); err != nil {
|
||||
if err := tx.DeleteTailnet(ctx, req.Msg.TailnetId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -104,7 +133,7 @@ func (s *Service) DeleteTailnet(ctx context.Context, req *api.DeleteTailnetReque
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.brokers(req.TailnetId).SignalUpdate()
|
||||
s.pubsub.Publish(req.Msg.TailnetId, &broker.Signal{})
|
||||
|
||||
return &api.DeleteTailnetResponse{}, nil
|
||||
return connect.NewResponse(&api.DeleteTailnetResponse{}), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/broker"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
)
|
||||
|
||||
func (s *Service) ListUsers(ctx context.Context, req *connect.Request[api.ListUsersRequest]) (*connect.Response[api.ListUsersResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
|
||||
tailnet, err := s.repository.GetTailnet(ctx, req.Msg.TailnetId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tailnet == nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("tailnet not found"))
|
||||
}
|
||||
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(tailnet.ID) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
users, err := s.repository.ListUsers(ctx, tailnet.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &api.ListUsersResponse{}
|
||||
for _, u := range users {
|
||||
resp.Users = append(resp.Users, &api.User{
|
||||
Id: u.ID,
|
||||
Name: u.Name,
|
||||
Role: string(tailnet.IAMPolicy.GetRole(u)),
|
||||
})
|
||||
}
|
||||
|
||||
return connect.NewResponse(resp), nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteUser(ctx context.Context, req *connect.Request[api.DeleteUserRequest]) (*connect.Response[api.DeleteUserResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
|
||||
if !principal.IsSystemAdmin() && principal.UserMatches(req.Msg.UserId) {
|
||||
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("unable delete yourself"))
|
||||
}
|
||||
|
||||
user, err := s.repository.GetUser(ctx, req.Msg.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, errors.New("user not found"))
|
||||
}
|
||||
|
||||
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(user.TailnetID) {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
|
||||
}
|
||||
|
||||
err = s.repository.Transaction(func(tx domain.Repository) error {
|
||||
if err := tx.DeleteMachineByUser(ctx, req.Msg.UserId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.DeleteApiKeysByUser(ctx, req.Msg.UserId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.DeleteAuthKeysByUser(ctx, req.Msg.UserId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.DeleteUser(ctx, req.Msg.UserId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.pubsub.Publish(user.TailnetID, &broker.Signal{})
|
||||
|
||||
return connect.NewResponse(&api.DeleteUserResponse{}), nil
|
||||
}
|
||||
@@ -74,29 +74,29 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
{{if .AuthMethods}}
|
||||
{{if .ProviderAvailable}}
|
||||
<div style="text-align: left; padding-bottom: 10px">
|
||||
<p><b>Authentication required</b></p>
|
||||
<small>Login with:</small>
|
||||
</div>
|
||||
<form method="post">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<ul class="selectionList">
|
||||
{{range .AuthMethods}}
|
||||
<li><button type="submit" name="s" value="{{.ID}}">{{.Name}}</button></li>
|
||||
{{end}}
|
||||
<li><button type="submit" name="s" value="true">OpenID</button></li>
|
||||
</ul>
|
||||
</form>
|
||||
<div style="text-align: left; padding-bottom: 10px; padding-top: 20px">
|
||||
<small>Or enter an <label for="ak">auth key</label> here:</small>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if not .AuthMethods}}
|
||||
{{if not .ProviderAvailable}}
|
||||
<div style="text-align: left; padding-bottom: 10px">
|
||||
<p><b>Authentication required</b></p>
|
||||
<small>Enter an <label for="ak">auth key</label> here:</small>
|
||||
</div>
|
||||
{{end}}
|
||||
<form method="post" style="text-align: right">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<p><input id="ak" name="ak" type="text"/></p>
|
||||
<div style="padding-top: 10px">
|
||||
<button type="submit">submit</button>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
padding: 10px;
|
||||
background: #379683;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
background: #fff;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
margin: 120px auto;
|
||||
padding: 25px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.selectionList li {
|
||||
position: relative;
|
||||
list-style: none;
|
||||
height: 45px;
|
||||
line-height: 45px;
|
||||
margin-bottom: 8px;
|
||||
background: #f2f2f2;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.selectionList {
|
||||
padding-top: 5px
|
||||
}
|
||||
|
||||
.selectionList li button {
|
||||
margin: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
height: 45px;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
<title>ionscale</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
{{if .ProviderAvailable}}
|
||||
<div style="text-align: left; padding-bottom: 10px">
|
||||
<p><b>Authentication required</b></p>
|
||||
<small>Login with:</small>
|
||||
</div>
|
||||
<form method="post">
|
||||
<ul class="selectionList">
|
||||
<li><button type="submit" name="s" value="true">OpenID</button></li>
|
||||
</ul>
|
||||
</form>
|
||||
{{end}}
|
||||
{{if not .ProviderAvailable}}
|
||||
<div style="text-align: center">
|
||||
<p><b>No authentication method available.</b></p>
|
||||
<small>contact your ionscale administrator for more information</small>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
padding: 10px;
|
||||
background: #379683;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
background: #fff;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
margin: 120px auto;
|
||||
padding: 25px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.selectionList li {
|
||||
position: relative;
|
||||
list-style: none;
|
||||
height: 45px;
|
||||
line-height: 45px;
|
||||
margin-bottom: 8px;
|
||||
background: #f2f2f2;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.selectionList li button {
|
||||
margin: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
<title>ionscale</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<div style="text-align: center">
|
||||
<p><b>Authentication successful</b></p>
|
||||
<small>but you're <b style="color: red">not</b> a valid tag owner for the requested tags</small>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -74,17 +74,41 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
{{if .SystemAdmin}}
|
||||
<div style="text-align: left; padding-bottom: 10px">
|
||||
<p><b>System Admin</b></p>
|
||||
<small>You are a member of the System Admin group:</small>
|
||||
</div>
|
||||
<form method="post">
|
||||
<input type="hidden" name="aid" value="{{.AccountID}}">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<ul class="selectionList">
|
||||
<li><button type="submit" name="sad" value="true">OK, continue as System Admin</button></li>
|
||||
</ul>
|
||||
</form>
|
||||
{{end}}
|
||||
{{if .Tailnets}}
|
||||
{{if .SystemAdmin}}
|
||||
<div style="text-align: left; padding-bottom: 10px; padding-top: 20px">
|
||||
<small>Or select your <b>tailnet</b>:</small>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if not .SystemAdmin}}
|
||||
<div style="text-align: left; padding-bottom: 10px;">
|
||||
<p><b>Tailnets</b></p>
|
||||
<small>Select your tailnet:</small>
|
||||
</div>
|
||||
{{end}}
|
||||
<form method="post">
|
||||
<input type="hidden" name="aid" value="{{.AccountID}}">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<ul class="selectionList">
|
||||
{{range .Tailnets}}
|
||||
<li><button type="submit" name="s" value="{{.ID}}">{{.Name}}</button></li>
|
||||
<li><button type="submit" name="tid" value="{{.ID}}">{{.Name}}</button></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+20
-19
@@ -1,11 +1,9 @@
|
||||
package ionscale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/key"
|
||||
"github.com/jsiebens/ionscale/internal/token"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
func LoadClientAuth(systemAdminKey string) (ClientAuth, error) {
|
||||
@@ -17,38 +15,41 @@ func LoadClientAuth(systemAdminKey string) (ClientAuth, error) {
|
||||
return &systemAdminTokenAuth{key: *k}, nil
|
||||
}
|
||||
|
||||
apiToken, err := TokenFromFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(apiToken) != 0 {
|
||||
return &apiTokenAuth{token: apiToken}, nil
|
||||
}
|
||||
|
||||
return &anonymous{}, nil
|
||||
}
|
||||
|
||||
type ClientAuth interface {
|
||||
credentials.PerRPCCredentials
|
||||
GetToken() (string, error)
|
||||
}
|
||||
|
||||
type anonymous struct {
|
||||
}
|
||||
|
||||
func (m *anonymous) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *anonymous) RequireTransportSecurity() bool {
|
||||
return false
|
||||
func (m *anonymous) GetToken() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
type systemAdminTokenAuth struct {
|
||||
key key.ServerPrivate
|
||||
}
|
||||
|
||||
func (m *systemAdminTokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
||||
t, err := token.GenerateSystemAdminToken(m.key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]string{
|
||||
"authorization": "Bearer " + t,
|
||||
}, nil
|
||||
func (m *systemAdminTokenAuth) GetToken() (string, error) {
|
||||
return token.GenerateSystemAdminToken(m.key)
|
||||
}
|
||||
|
||||
func (m *systemAdminTokenAuth) RequireTransportSecurity() bool {
|
||||
return false
|
||||
type apiTokenAuth struct {
|
||||
token string
|
||||
}
|
||||
|
||||
func (m *apiTokenAuth) GetToken() (string, error) {
|
||||
return m.token, nil
|
||||
}
|
||||
|
||||
@@ -1,49 +1,35 @@
|
||||
package ionscale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"github.com/jsiebens/ionscale/pkg/gen/api"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"io"
|
||||
"net/url"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1/ionscalev1connect"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func NewClient(clientAuth ClientAuth, serverURL string, insecureSkipVerify bool, useGrpcWebProxy bool) (api.IonscaleClient, io.Closer, error) {
|
||||
parsedUrl, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
func NewClient(clientAuth ClientAuth, serverURL string, insecureSkipVerify bool) (api.IonscaleServiceClient, error) {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: insecureSkipVerify,
|
||||
}
|
||||
|
||||
if parsedUrl.Scheme == "" {
|
||||
parsedUrl.Scheme = "https"
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
},
|
||||
}
|
||||
|
||||
if useGrpcWebProxy {
|
||||
conn, err := NewGrpcWebProxy(*parsedUrl, insecureSkipVerify).Dial(grpc.WithPerRPCCredentials(clientAuth))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return api.NewIonscaleClient(conn), conn, nil
|
||||
}
|
||||
|
||||
var targetAddr = parsedUrl.Host
|
||||
if parsedUrl.Port() == "" {
|
||||
targetAddr = targetAddr + ":443"
|
||||
}
|
||||
|
||||
var transportCreds = credentials.NewTLS(&tls.Config{InsecureSkipVerify: insecureSkipVerify})
|
||||
|
||||
if parsedUrl.Scheme != "https" {
|
||||
transportCreds = insecure.NewCredentials()
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(targetAddr, grpc.WithPerRPCCredentials(clientAuth), grpc.WithTransportCredentials(transportCreds))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return api.NewIonscaleClient(conn), conn, nil
|
||||
interceptors := connect.WithInterceptors(NewAuthenticationInterceptor(clientAuth))
|
||||
return api.NewIonscaleServiceClient(client, serverURL, interceptors), nil
|
||||
}
|
||||
|
||||
func NewAuthenticationInterceptor(clientAuth ClientAuth) connect.UnaryInterceptorFunc {
|
||||
return func(next connect.UnaryFunc) connect.UnaryFunc {
|
||||
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
||||
token, _ := clientAuth.GetToken()
|
||||
req.Header().Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
return next(ctx, req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package ionscale
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultDir string = "~/.ionscale"
|
||||
DefaultPermissions os.FileMode = 0700
|
||||
)
|
||||
|
||||
func TokenFromFile() (string, error) {
|
||||
return valueFromFile("token")
|
||||
}
|
||||
|
||||
func TailnetFromFile() (uint64, error) {
|
||||
v, err := valueFromFile("tailnet_id")
|
||||
if v == "" {
|
||||
return 0, nil
|
||||
}
|
||||
p, err := strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
|
||||
func valueFromFile(name string) (string, error) {
|
||||
file, err := EnsureFile(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
func SessionToFile(token string, tailnetID *uint64) error {
|
||||
if err := TokenToFile(token); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := TailnetToFile(tailnetID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TokenToFile(token string) error {
|
||||
file, err := EnsureFile("token")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(file, []byte(token), 0600)
|
||||
}
|
||||
|
||||
func TailnetToFile(id *uint64) error {
|
||||
file, err := EnsureFile("tailnet_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var v = ""
|
||||
|
||||
if id != nil {
|
||||
v = strconv.FormatUint(*id, 10)
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(file, []byte(v), 0600)
|
||||
}
|
||||
|
||||
func ConfigDir() string {
|
||||
return DefaultDir
|
||||
}
|
||||
|
||||
func EnsureFile(file string) (string, error) {
|
||||
permission := DefaultPermissions
|
||||
dir := ConfigDir()
|
||||
dirPath, err := homedir.Expand(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
filePath := path.Clean(filepath.Join(dirPath, file))
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), permission); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
}
|
||||
|
||||
return filePath, nil
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
package ionscale
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
*grpc.ClientConn
|
||||
cls io.Closer
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
_ = c.ClientConn.Close()
|
||||
_ = c.cls.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewGrpcWebProxy(serverUrl url.URL, insecureSkipVerify bool) *grpcWebProxy {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: insecureSkipVerify,
|
||||
}
|
||||
|
||||
httpClient := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
|
||||
return &grpcWebProxy{
|
||||
serverUrl: serverUrl,
|
||||
proxyMutex: &sync.Mutex{},
|
||||
httpClient: httpClient,
|
||||
}
|
||||
}
|
||||
|
||||
type grpcWebProxy struct {
|
||||
serverUrl url.URL
|
||||
proxyMutex *sync.Mutex
|
||||
proxyListener net.Listener
|
||||
proxyServer *grpc.Server
|
||||
proxyUsersCount int
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
const (
|
||||
frameHeaderLength = 5
|
||||
endOfStreamFlag = 128
|
||||
)
|
||||
|
||||
type noopCodec struct{}
|
||||
|
||||
func (noopCodec) Marshal(v interface{}) ([]byte, error) {
|
||||
return v.([]byte), nil
|
||||
}
|
||||
|
||||
func (noopCodec) Unmarshal(data []byte, v interface{}) error {
|
||||
pointer := v.(*[]byte)
|
||||
*pointer = data
|
||||
return nil
|
||||
}
|
||||
|
||||
func (noopCodec) Name() string {
|
||||
return "proto"
|
||||
}
|
||||
|
||||
func toFrame(msg []byte) []byte {
|
||||
frame := append([]byte{0, 0, 0, 0}, msg...)
|
||||
binary.BigEndian.PutUint32(frame, uint32(len(msg)))
|
||||
frame = append([]byte{0}, frame...)
|
||||
return frame
|
||||
}
|
||||
|
||||
func (c *grpcWebProxy) Dial(opts ...grpc.DialOption) (*Conn, error) {
|
||||
addr, i, err := c.useGRPCProxy()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialer := func(ctx context.Context, address string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, addr.Network(), address)
|
||||
}
|
||||
|
||||
opts = append(opts,
|
||||
grpc.WithBlock(),
|
||||
grpc.FailOnNonTempDialError(true),
|
||||
grpc.WithContextDialer(dialer),
|
||||
grpc.WithInsecure(), // we are handling TLS, so tell grpc not to
|
||||
grpc.WithKeepaliveParams(keepalive.ClientParameters{Time: 10 * time.Second}),
|
||||
)
|
||||
|
||||
conn, err := grpc.DialContext(context.Background(), addr.String(), opts...)
|
||||
|
||||
return &Conn{ClientConn: conn, cls: i}, err
|
||||
}
|
||||
|
||||
func (c *grpcWebProxy) executeRequest(fullMethodName string, msg []byte, md metadata.MD) (*http.Response, error) {
|
||||
requestURL := fmt.Sprintf("%s%s", c.serverUrl.String(), fullMethodName)
|
||||
req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader(toFrame(msg)))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range md {
|
||||
if strings.HasPrefix(k, ":") {
|
||||
continue
|
||||
}
|
||||
for i := range v {
|
||||
req.Header.Set(k, v[i])
|
||||
}
|
||||
}
|
||||
req.Header.Set("content-type", "application/grpc-web+proto")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s %s failed with status code %d", req.Method, req.URL, resp.StatusCode)
|
||||
}
|
||||
var code codes.Code
|
||||
if statusStr := resp.Header.Get("Grpc-Status"); statusStr != "" {
|
||||
statusInt, err := strconv.ParseUint(statusStr, 10, 32)
|
||||
if err != nil {
|
||||
code = codes.Unknown
|
||||
} else {
|
||||
code = codes.Code(statusInt)
|
||||
}
|
||||
if code != codes.OK {
|
||||
return nil, status.Error(code, resp.Header.Get("Grpc-Message"))
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *grpcWebProxy) startGRPCProxy() (*grpc.Server, net.Listener, error) {
|
||||
serverAddr := fmt.Sprintf("%s/ionscale-%s.sock", os.TempDir(), util.RandStringBytes(8))
|
||||
ln, err := net.Listen("unix", serverAddr)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
proxySrv := grpc.NewServer(
|
||||
grpc.ForceServerCodec(&noopCodec{}),
|
||||
grpc.UnknownServiceHandler(func(srv interface{}, stream grpc.ServerStream) error {
|
||||
fullMethodName, ok := grpc.MethodFromServerStream(stream)
|
||||
if !ok {
|
||||
return fmt.Errorf("Unable to get method name from stream context.")
|
||||
}
|
||||
msg := make([]byte, 0)
|
||||
err := stream.RecvMsg(&msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
md, _ := metadata.FromIncomingContext(stream.Context())
|
||||
|
||||
resp, err := c.executeRequest(fullMethodName, msg, md)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-stream.Context().Done()
|
||||
safeClose(resp.Body)
|
||||
}()
|
||||
defer safeClose(resp.Body)
|
||||
c.httpClient.CloseIdleConnections()
|
||||
|
||||
for {
|
||||
header := make([]byte, frameHeaderLength)
|
||||
if _, err := io.ReadAtLeast(resp.Body, header, frameHeaderLength); err != nil {
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if header[0] == endOfStreamFlag {
|
||||
return nil
|
||||
}
|
||||
length := int(binary.BigEndian.Uint32(header[1:frameHeaderLength]))
|
||||
data := make([]byte, length)
|
||||
|
||||
if read, err := io.ReadAtLeast(resp.Body, data, length); err != nil {
|
||||
if err != io.EOF {
|
||||
return err
|
||||
} else if read < length {
|
||||
return io.ErrUnexpectedEOF
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := stream.SendMsg(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
}))
|
||||
go func() {
|
||||
_ = proxySrv.Serve(ln)
|
||||
}()
|
||||
return proxySrv, ln, nil
|
||||
}
|
||||
|
||||
func (c *grpcWebProxy) useGRPCProxy() (net.Addr, io.Closer, error) {
|
||||
c.proxyMutex.Lock()
|
||||
defer c.proxyMutex.Unlock()
|
||||
|
||||
if c.proxyListener == nil {
|
||||
var err error
|
||||
c.proxyServer, c.proxyListener, err = c.startGRPCProxy()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
c.proxyUsersCount = c.proxyUsersCount + 1
|
||||
|
||||
return c.proxyListener.Addr(), NewCloser(func() error {
|
||||
c.proxyMutex.Lock()
|
||||
defer c.proxyMutex.Unlock()
|
||||
c.proxyUsersCount = c.proxyUsersCount - 1
|
||||
if c.proxyUsersCount == 0 {
|
||||
c.proxyServer.Stop()
|
||||
c.proxyListener = nil
|
||||
c.proxyServer = nil
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
type Closer interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
type inlineCloser struct {
|
||||
close func() error
|
||||
}
|
||||
|
||||
func (c *inlineCloser) Close() error {
|
||||
return c.close()
|
||||
}
|
||||
|
||||
func NewCloser(close func() error) Closer {
|
||||
return &inlineCloser{close: close}
|
||||
}
|
||||
|
||||
func safeClose(c Closer) {
|
||||
if c != nil {
|
||||
_ = c.Close()
|
||||
}
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.27.1
|
||||
// protoc v3.17.3
|
||||
// source: api/acl.proto
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
_ "google.golang.org/protobuf/types/known/durationpb"
|
||||
_ "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
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 GetACLPolicyRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
TailnetId uint64 `protobuf:"varint,1,opt,name=tailnet_id,json=tailnetId,proto3" json:"tailnet_id,omitempty"`
|
||||
}
|
||||
|
||||
func (x *GetACLPolicyRequest) Reset() {
|
||||
*x = GetACLPolicyRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_api_acl_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GetACLPolicyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetACLPolicyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetACLPolicyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_acl_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetACLPolicyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetACLPolicyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_api_acl_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *GetACLPolicyRequest) GetTailnetId() uint64 {
|
||||
if x != nil {
|
||||
return x.TailnetId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type GetACLPolicyResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Value []byte `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (x *GetACLPolicyResponse) Reset() {
|
||||
*x = GetACLPolicyResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_api_acl_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GetACLPolicyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetACLPolicyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetACLPolicyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_acl_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetACLPolicyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetACLPolicyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_api_acl_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *GetACLPolicyResponse) GetValue() []byte {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SetACLPolicyRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
TailnetId uint64 `protobuf:"varint,1,opt,name=tailnet_id,json=tailnetId,proto3" json:"tailnet_id,omitempty"`
|
||||
Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SetACLPolicyRequest) Reset() {
|
||||
*x = SetACLPolicyRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_api_acl_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SetACLPolicyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SetACLPolicyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SetACLPolicyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_acl_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SetACLPolicyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SetACLPolicyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_api_acl_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *SetACLPolicyRequest) GetTailnetId() uint64 {
|
||||
if x != nil {
|
||||
return x.TailnetId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SetACLPolicyRequest) GetValue() []byte {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SetACLPolicyResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
}
|
||||
|
||||
func (x *SetACLPolicyResponse) Reset() {
|
||||
*x = SetACLPolicyResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_api_acl_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SetACLPolicyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SetACLPolicyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SetACLPolicyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_acl_proto_msgTypes[3]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SetACLPolicyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*SetACLPolicyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_api_acl_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
var File_api_acl_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_api_acl_proto_rawDesc = []byte{
|
||||
0x0a, 0x0d, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x63, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
|
||||
0x03, 0x61, 0x70, 0x69, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x66, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x22, 0x34, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x41, 0x43, 0x4c, 0x50, 0x6f,
|
||||
0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x74,
|
||||
0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52,
|
||||
0x09, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x49, 0x64, 0x22, 0x2c, 0x0a, 0x14, 0x47, 0x65,
|
||||
0x74, 0x41, 0x43, 0x4c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x4a, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x41,
|
||||
0x43, 0x4c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||
0x1d, 0x0a, 0x0a, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x61, 0x69, 0x6c, 0x6e, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14,
|
||||
0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76,
|
||||
0x61, 0x6c, 0x75, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x41, 0x43, 0x4c, 0x50, 0x6f,
|
||||
0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2a, 0x5a, 0x28,
|
||||
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x73, 0x69, 0x65, 0x62,
|
||||
0x65, 0x6e, 0x73, 0x2f, 0x69, 0x6f, 0x6e, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x70, 0x6b, 0x67,
|
||||
0x2f, 0x67, 0x65, 0x6e, 0x3b, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_api_acl_proto_rawDescOnce sync.Once
|
||||
file_api_acl_proto_rawDescData = file_api_acl_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_api_acl_proto_rawDescGZIP() []byte {
|
||||
file_api_acl_proto_rawDescOnce.Do(func() {
|
||||
file_api_acl_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_acl_proto_rawDescData)
|
||||
})
|
||||
return file_api_acl_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_api_acl_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
||||
var file_api_acl_proto_goTypes = []interface{}{
|
||||
(*GetACLPolicyRequest)(nil), // 0: api.GetACLPolicyRequest
|
||||
(*GetACLPolicyResponse)(nil), // 1: api.GetACLPolicyResponse
|
||||
(*SetACLPolicyRequest)(nil), // 2: api.SetACLPolicyRequest
|
||||
(*SetACLPolicyResponse)(nil), // 3: api.SetACLPolicyResponse
|
||||
}
|
||||
var file_api_acl_proto_depIdxs = []int32{
|
||||
0, // [0:0] is the sub-list for method output_type
|
||||
0, // [0:0] 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_api_acl_proto_init() }
|
||||
func file_api_acl_proto_init() {
|
||||
if File_api_acl_proto != nil {
|
||||
return
|
||||
}
|
||||
file_api_ref_proto_init()
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_api_acl_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GetACLPolicyRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_api_acl_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GetACLPolicyResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_api_acl_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*SetACLPolicyRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_api_acl_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*SetACLPolicyResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_api_acl_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 4,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_api_acl_proto_goTypes,
|
||||
DependencyIndexes: file_api_acl_proto_depIdxs,
|
||||
MessageInfos: file_api_acl_proto_msgTypes,
|
||||
}.Build()
|
||||
File_api_acl_proto = out.File
|
||||
file_api_acl_proto_rawDesc = nil
|
||||
file_api_acl_proto_goTypes = nil
|
||||
file_api_acl_proto_depIdxs = nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user