mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-08 14:39:09 +02:00
[management] switch to libopenapi for managing of openapi-based api (#8056)
* use libopenapi OpenAPI generator Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * added a handler for v1alpha1/peers Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * wire up request validator Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * wired up spec-based validation Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * use sync validation; set base url to v1alpha1 Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * moved stuff around, extracted runtime libopenapu deps into runtime_tooling Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * testing /peers path params Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * fixed tests Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * added user schemas and paths Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * use api validation in tests Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * post-merge fixes Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * deleted tmp command used to test validator integration Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * require request body in post/put requests in order to force validation Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * making linter happy Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * switch to api/v1alpha1 Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * go mod tidy Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * keep the original order of middleware: metrics, cors, then auth Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * return 422 on validation errors Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * cleanups Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * more cleanups Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * v1alpha1 spec cleanups Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * no need for a double-pointer Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * more linter fixes Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * removed more double pointers Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * use di to inject api_v0 and api_v1 http routers Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * do not re-add middleware on repeated call to ApiHandler Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * calling ApiRouter() now also calls ApiV1Router() Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> --------- Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
This commit is contained in:
@@ -61,7 +61,7 @@ require (
|
||||
github.com/go-jose/go-jose/v4 v4.1.4
|
||||
github.com/go-ole/go-ole v1.3.0
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/goccy/go-yaml v1.19.2
|
||||
github.com/godbus/dbus/v5 v5.2.2
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/google/go-cmp v0.7.0
|
||||
@@ -92,6 +92,8 @@ require (
|
||||
github.com/ory/dockertest/v4 v4.0.0
|
||||
github.com/oschwald/maxminddb-golang v1.12.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pb33f/libopenapi v0.41.2
|
||||
github.com/pb33f/libopenapi-validator v0.15.0
|
||||
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203
|
||||
github.com/pion/ice/v4 v4.0.0-00010101000000-000000000000
|
||||
github.com/pion/logging v0.2.4
|
||||
@@ -135,7 +137,7 @@ require (
|
||||
golang.org/x/mod v0.39.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/term v0.45.0
|
||||
golang.org/x/time v0.15.0
|
||||
google.golang.org/api v0.276.0
|
||||
@@ -183,9 +185,12 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
|
||||
github.com/aws/smithy-go v1.23.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad // indirect
|
||||
github.com/beevik/etree v1.6.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/buger/jsonparser v1.1.2 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
@@ -213,12 +218,13 @@ require (
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||
github.com/go-openapi/errors v0.22.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.23.2 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/loads v0.22.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/strfmt v0.23.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.26.1 // indirect
|
||||
github.com/go-openapi/validate v0.24.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
@@ -297,6 +303,9 @@ require (
|
||||
github.com/openbao/openbao/api/v2 v2.5.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pb33f/go-yaml v0.1.1 // indirect
|
||||
github.com/pb33f/jsonpath v0.8.4 // indirect
|
||||
github.com/pb33f/ordered-map/v2 v2.3.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.10 // indirect
|
||||
@@ -314,6 +323,7 @@ require (
|
||||
github.com/russellhaering/goxmldsig v1.6.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||
|
||||
@@ -93,10 +93,16 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c=
|
||||
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad h1:3swAvbzgfaI6nKuDDU7BiKfZRdF+h2ZwKgMHd8Ha4t8=
|
||||
github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad/go.mod h1:9+nBLYNWkvPcq9ep0owWUsPTLgL9ZXTsZWcCSVGGLJ0=
|
||||
github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE=
|
||||
github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow=
|
||||
github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
@@ -104,6 +110,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
|
||||
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/c-robinson/iplib v1.0.3 h1:NG0UF0GoEsrC1/vyfX1Lx2Ss7CySWl3KqqXh3q4DdPU=
|
||||
github.com/c-robinson/iplib v1.0.3/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
|
||||
github.com/caarlos0/env/v11 v11.4.1 h1:fYwH0sWEsBSMPG7t4e/PEfTFzrWrpjyygXyUnWiSwEw=
|
||||
@@ -155,6 +163,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
|
||||
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
@@ -220,8 +230,8 @@ github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC0
|
||||
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
|
||||
github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg=
|
||||
github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
|
||||
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||
github.com/go-openapi/jsonpointer v0.23.2 h1:DK7R/3zAt4xTytxNkw7jARGPFI7rkaSsii58n8X45x0=
|
||||
github.com/go-openapi/jsonpointer v0.23.2/go.mod h1:noUOckXtq7b4bVkqw0sbHKieq9uEZRN7p6EF/dalc4w=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
|
||||
@@ -232,6 +242,10 @@ github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMg
|
||||
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
|
||||
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE=
|
||||
github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc=
|
||||
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
|
||||
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
||||
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
@@ -259,8 +273,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
@@ -561,6 +575,18 @@ github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq5
|
||||
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pb33f/go-yaml v0.1.1 h1:yWGmhVdwzionRnSc/hEJwcfq6PaZL2QdYzyReXBbX7Q=
|
||||
github.com/pb33f/go-yaml v0.1.1/go.mod h1:QvWdkHP5VjFYAeHMQUkaPQqREaQnBNOXmWzAjNrnSdo=
|
||||
github.com/pb33f/jsonpath v0.8.4 h1:Yx7fxsKl8scgERL22WhKDs2c6gYNNUonPSWEG8Y194s=
|
||||
github.com/pb33f/jsonpath v0.8.4/go.mod h1:eO6mgdhw5RgzCxm/bXTi75VUs0/cCYJzdGjMCCUEI6M=
|
||||
github.com/pb33f/libopenapi v0.41.2 h1:7/KNzJJ0o5fC/tYxDYcHjziKfg0bkBpFUfI2UDbChAo=
|
||||
github.com/pb33f/libopenapi v0.41.2/go.mod h1:mUldESjO6ownR1TCcywvoYp7e4v/bCdQyiiIqsgWne0=
|
||||
github.com/pb33f/libopenapi-validator v0.15.0 h1:5wl7/tpyewqzZ7Y5/M9CZjojlaaK2DYKPZYGIvPq7SI=
|
||||
github.com/pb33f/libopenapi-validator v0.15.0/go.mod h1:P52RfZsY/+oWjRL52mpQ8Icwual6rtkJt1qOWdAe4eM=
|
||||
github.com/pb33f/ordered-map/v2 v2.3.2 h1:wDyaZ2Pv9QLh64X4utCeD5Zoi9nIv2aW3lwv172O5PQ=
|
||||
github.com/pb33f/ordered-map/v2 v2.3.2/go.mod h1:1OhFrXu3OYw3kM+FXF+Ug3ugmmZ9LE8z0JfQMLvtV0w=
|
||||
github.com/pb33f/testify v0.1.1 h1:mnHe7uxKt8dyNYEGUspow72VjD264DB5rOJq4bz5tJw=
|
||||
github.com/pb33f/testify v0.1.1/go.mod h1:keghMqOLECF1ENzESnfM+cwPcKN5uhTOpvbtjgL6aZg=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 h1:E7Kmf11E4K7B5hDti2K2NqPb1nlYlGYsu02S1JNd/Bs=
|
||||
@@ -638,6 +664,8 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
|
||||
@@ -790,6 +818,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
@@ -828,6 +857,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
@@ -849,8 +879,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -897,6 +927,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
@@ -912,6 +943,7 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/magefile/mage/mg"
|
||||
"github.com/netbirdio/netbird/shared/management/http/apiv1alpha1"
|
||||
)
|
||||
|
||||
var apiPath = filepath.Join("shared", "management", "http", "apiv1alpha1")
|
||||
|
||||
type Openapi mg.Namespace
|
||||
|
||||
func (Openapi) GenerateV1Bindings(generateflags *string) error {
|
||||
bundle, model, err := apiv1alpha1.GenerateV1ApiBindings(apiv1alpha1.ApiPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(apiPath, "bundle.yaml"), bundle, 0644) //nolint:gosec
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
generated, err := apiv1alpha1.GenerateV1Schema(model)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filepath.Join(apiPath, "types.gen.go"), generated.Source, 0644) //nolint:gosec
|
||||
}
|
||||
@@ -36,9 +36,11 @@ import (
|
||||
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
activitystore "github.com/netbirdio/netbird/management/server/activity/store"
|
||||
"github.com/netbirdio/netbird/management/server/api/v1alpha1"
|
||||
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
||||
nbContext "github.com/netbirdio/netbird/management/server/context"
|
||||
nbhttp "github.com/netbirdio/netbird/management/server/http"
|
||||
"github.com/netbirdio/netbird/management/server/http/middleware"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
@@ -47,7 +49,10 @@ import (
|
||||
"github.com/netbirdio/netbird/util/crypt"
|
||||
)
|
||||
|
||||
const apiPrefix = "/api"
|
||||
const (
|
||||
apiPrefix = "/api"
|
||||
apiV1Prefix = "/api/v1alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
kaep = keepalive.EnforcementPolicy{
|
||||
@@ -158,13 +163,31 @@ func (s *BaseServer) EventStore() activity.Store {
|
||||
}
|
||||
|
||||
func (s *BaseServer) APIHandler() http.Handler {
|
||||
return Create(s, func() http.Handler {
|
||||
httpAPIHandler, err := nbhttp.NewAPIHandler(context.Background(), s.Router(), s.AccountManager(), s.NetworksManager(), s.ResourcesManager(), s.RoutesManager(), s.GroupsManager(), s.GeoLocationManager(), s.AuthManager(), s.Metrics(), s.PermissionsManager(), s.SettingsManager(), s.ZonesManager(), s.RecordsManager(), s.NetworkMapController(), s.IdpManager(), s.ServiceManager(), s.ReverseProxyDomainManager(), s.AccessLogsManager(), s.ReverseProxyGRPCServer(), s.Config.ReverseProxy.TrustedHTTPProxies, s.RateLimiter(), s.IsValidChildAccount, s.AgentNetworkManager(), nil)
|
||||
_ = CreateNamed(s, "http_v1api", func() http.Handler {
|
||||
apiv1Router := s.ApiV1Router()
|
||||
_, err := v1alpha1.NewAPIV1Handler(context.Background(), apiv1Router, s.AccountManager(), s.NetworkMapController(), s.PermissionsManager())
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create API handler: %v", err)
|
||||
}
|
||||
return httpAPIHandler
|
||||
|
||||
return apiv1Router
|
||||
})
|
||||
|
||||
_ = CreateNamed(s, "http_v0api", func() http.Handler {
|
||||
apiRouter := s.ApiRouter()
|
||||
_, err := nbhttp.NewAPIHandler(
|
||||
context.Background(), apiRouter, s.AccountManager(), s.NetworksManager(), s.ResourcesManager(), s.RoutesManager(),
|
||||
s.GroupsManager(), s.GeoLocationManager(), s.PermissionsManager(), s.SettingsManager(), s.ZonesManager(),
|
||||
s.RecordsManager(), s.NetworkMapController(), s.IdpManager(), s.ServiceManager(), s.ReverseProxyDomainManager(),
|
||||
s.AccessLogsManager(), s.ReverseProxyGRPCServer(), s.Config.ReverseProxy.TrustedHTTPProxies,
|
||||
s.AgentNetworkManager(), nil)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create API handler: %v", err)
|
||||
}
|
||||
return apiRouter
|
||||
})
|
||||
|
||||
return s.Router()
|
||||
}
|
||||
|
||||
// IDPHandler returns the HTTP handler for the embedded IdP (Dex), or nil if
|
||||
@@ -177,9 +200,28 @@ func (s *BaseServer) IDPHandler() http.Handler {
|
||||
return cors.AllowAll().Handler(embeddedIdP.Handler())
|
||||
}
|
||||
|
||||
// Router returns the root HTTP router with the shared API middleware applied.
|
||||
func (s *BaseServer) Router() *mux.Router {
|
||||
return Create(s, func() *mux.Router {
|
||||
return mux.NewRouter().PathPrefix(apiPrefix).Subrouter()
|
||||
router := mux.NewRouter()
|
||||
router.Use(middleware.BuildMiddleware(s.RateLimiter(), s.AuthManager(), s.AccountManager(), s.Metrics(), s.IsValidChildAccount)...)
|
||||
return router
|
||||
})
|
||||
}
|
||||
|
||||
// ApiV1Router returns the subrouter for the versioned API under apiV1Prefix.
|
||||
func (s *BaseServer) ApiV1Router() *mux.Router {
|
||||
return CreateNamed(s, "apiv1_router", func() *mux.Router {
|
||||
return s.Router().PathPrefix(apiV1Prefix).Subrouter()
|
||||
})
|
||||
}
|
||||
|
||||
// ApiRouter returns the subrouter for the unversioned API under apiPrefix.
|
||||
func (s *BaseServer) ApiRouter() *mux.Router {
|
||||
return CreateNamed(s, "apiv0_router", func() *mux.Router {
|
||||
// The nested versioned prefix must be registered before the broader apiPrefix.
|
||||
s.ApiV1Router()
|
||||
return s.Router().PathPrefix(apiPrefix).Subrouter()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//go:build integration
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
@@ -195,7 +193,7 @@ func TestValidateSession_UserAllowed(t *testing.T) {
|
||||
|
||||
resp, err := setup.proxyService.ValidateSession(context.Background(), &proto.ValidateSessionRequest{
|
||||
Domain: "test-proxy.example.com",
|
||||
SessionToken: token,
|
||||
SessionToken: token, //nolint:staticcheck
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -216,7 +214,7 @@ func TestValidateSession_UserNotInAllowedGroup(t *testing.T) {
|
||||
|
||||
resp, err := setup.proxyService.ValidateSession(context.Background(), &proto.ValidateSessionRequest{
|
||||
Domain: "restricted-proxy.example.com",
|
||||
SessionToken: token,
|
||||
SessionToken: token, //nolint:staticcheck
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -240,7 +238,7 @@ func TestValidateSession_PendingApprovalUserDenied(t *testing.T) {
|
||||
|
||||
resp, err := setup.proxyService.ValidateSession(context.Background(), &proto.ValidateSessionRequest{
|
||||
Domain: "restricted-proxy.example.com",
|
||||
SessionToken: token,
|
||||
SessionToken: token, //nolint:staticcheck
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -265,7 +263,7 @@ func TestValidateSession_PendingApprovalUserInAllUsersGroupDenied(t *testing.T)
|
||||
|
||||
resp, err := setup.proxyService.ValidateSession(context.Background(), &proto.ValidateSessionRequest{
|
||||
Domain: "all-users-proxy.example.com",
|
||||
SessionToken: token,
|
||||
SessionToken: token, //nolint:staticcheck
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -288,7 +286,7 @@ func TestValidateSession_BlockedUserDenied(t *testing.T) {
|
||||
|
||||
resp, err := setup.proxyService.ValidateSession(context.Background(), &proto.ValidateSessionRequest{
|
||||
Domain: "restricted-proxy.example.com",
|
||||
SessionToken: token,
|
||||
SessionToken: token, //nolint:staticcheck
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -312,7 +310,7 @@ func TestValidateSession_UserAllowedAfterApproval(t *testing.T) {
|
||||
token := createSessionToken(t, proxy.SessionPrivateKey, pendingUserID, "restricted-proxy.example.com")
|
||||
req := &proto.ValidateSessionRequest{
|
||||
Domain: "restricted-proxy.example.com",
|
||||
SessionToken: token,
|
||||
SessionToken: token, //nolint:staticcheck,
|
||||
}
|
||||
|
||||
resp, err := setup.proxyService.ValidateSession(ctx, req)
|
||||
@@ -345,7 +343,7 @@ func TestValidateSession_UserInDifferentAccount(t *testing.T) {
|
||||
|
||||
resp, err := setup.proxyService.ValidateSession(context.Background(), &proto.ValidateSessionRequest{
|
||||
Domain: "test-proxy.example.com",
|
||||
SessionToken: token,
|
||||
SessionToken: token, //nolint:staticcheck,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -364,7 +362,7 @@ func TestValidateSession_UserNotFound(t *testing.T) {
|
||||
|
||||
resp, err := setup.proxyService.ValidateSession(context.Background(), &proto.ValidateSessionRequest{
|
||||
Domain: "test-proxy.example.com",
|
||||
SessionToken: token,
|
||||
SessionToken: token, //nolint:staticcheck,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -383,7 +381,7 @@ func TestValidateSession_ProxyNotFound(t *testing.T) {
|
||||
|
||||
resp, err := setup.proxyService.ValidateSession(context.Background(), &proto.ValidateSessionRequest{
|
||||
Domain: "unknown-proxy.example.com",
|
||||
SessionToken: token,
|
||||
SessionToken: token, //nolint:staticcheck,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -397,7 +395,7 @@ func TestValidateSession_InvalidToken(t *testing.T) {
|
||||
|
||||
resp, err := setup.proxyService.ValidateSession(context.Background(), &proto.ValidateSessionRequest{
|
||||
Domain: "test-proxy.example.com",
|
||||
SessionToken: "invalid-token",
|
||||
SessionToken: "invalid-token", //nolint:staticcheck,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -410,7 +408,7 @@ func TestValidateSession_MissingDomain(t *testing.T) {
|
||||
defer setup.cleanup()
|
||||
|
||||
resp, err := setup.proxyService.ValidateSession(context.Background(), &proto.ValidateSessionRequest{
|
||||
SessionToken: "some-token",
|
||||
SessionToken: "some-token", //nolint:staticcheck,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -491,15 +489,15 @@ func (m *testValidateSessionServiceManager) GetAllServices(_ context.Context, _,
|
||||
}
|
||||
|
||||
func (m *testValidateSessionServiceManager) GetService(_ context.Context, _, _, _ string) (*service.Service, error) {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
func (m *testValidateSessionServiceManager) CreateService(_ context.Context, _, _ string, _ *service.Service) (*service.Service, error) {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
func (m *testValidateSessionServiceManager) UpdateService(_ context.Context, _, _ string, _ *service.Service) (*service.Service, error) {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
func (m *testValidateSessionServiceManager) DeleteService(_ context.Context, _, _, _ string) error {
|
||||
@@ -543,7 +541,7 @@ func (m *testValidateSessionServiceManager) GetServiceIDByTargetID(_ context.Con
|
||||
}
|
||||
|
||||
func (m *testValidateSessionServiceManager) CreateServiceFromPeer(_ context.Context, _, _ string, _ *service.ExposeServiceRequest) (*service.ExposeServiceResponse, error) {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
func (m *testValidateSessionServiceManager) RenewServiceFromPeer(_ context.Context, _, _, _ string) error {
|
||||
@@ -571,7 +569,7 @@ func (m *testValidateSessionServiceManager) DeleteAccountCluster(_ context.Conte
|
||||
type testValidateSessionProxyManager struct{}
|
||||
|
||||
func (m *testValidateSessionProxyManager) Connect(_ context.Context, _, _, _, _, _ string, _ *string, _ *proxy.Capabilities) (*proxy.Proxy, error) {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
func (m *testValidateSessionProxyManager) Disconnect(_ context.Context, _, _ string) error {
|
||||
@@ -603,7 +601,7 @@ func (m *testValidateSessionProxyManager) CleanupStale(_ context.Context, _ time
|
||||
}
|
||||
|
||||
func (m *testValidateSessionProxyManager) GetAccountProxy(_ context.Context, _ string) (*proxy.Proxy, error) {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
func (m *testValidateSessionProxyManager) CountAccountProxies(_ context.Context, _ string) (int64, error) {
|
||||
@@ -634,6 +632,10 @@ func (m *testValidateSessionProxyManager) ClusterSupportsPrivate(_ context.Conte
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *testValidateSessionProxyManager) ClusterAllProxiesPrivate(_ context.Context, _ string) *bool {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *testValidateSessionProxyManager) ClusterSupportsSessionCode(_ context.Context, _ string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package groups
|
||||
|
||||
import (
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/management/http/apiv1alpha1"
|
||||
)
|
||||
|
||||
func ToGroupsInfoMap(groups []*types.Group, idCount int) map[string][]apiv1alpha1.GroupMinimum {
|
||||
groupsInfoMap := make(map[string][]apiv1alpha1.GroupMinimum, idCount)
|
||||
groupsChecked := make(map[string]struct{}, len(groups)) // not sure why this is needed (left over from old implementation)
|
||||
for _, group := range groups {
|
||||
_, ok := groupsChecked[group.ID]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
|
||||
groupsChecked[group.ID] = struct{}{}
|
||||
for _, pk := range group.Peers {
|
||||
info := apiv1alpha1.GroupMinimum{
|
||||
Id: group.ID,
|
||||
Name: group.Name,
|
||||
PeersCount: len(group.Peers),
|
||||
ResourcesCount: len(group.Resources),
|
||||
}
|
||||
groupsInfoMap[pk] = append(groupsInfoMap[pk], info)
|
||||
}
|
||||
for _, rk := range group.Resources {
|
||||
info := apiv1alpha1.GroupMinimum{
|
||||
Id: group.ID,
|
||||
Name: group.Name,
|
||||
PeersCount: len(group.Peers),
|
||||
ResourcesCount: len(group.Resources),
|
||||
}
|
||||
groupsInfoMap[rk.ID] = append(groupsInfoMap[rk.ID], info)
|
||||
}
|
||||
}
|
||||
return groupsInfoMap
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||
"github.com/netbirdio/netbird/management/server/account"
|
||||
"github.com/netbirdio/netbird/shared/management/http/apiv1alpha1"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/permissions"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/api/v1alpha1/peers"
|
||||
"github.com/netbirdio/netbird/management/server/api/v1alpha1/users"
|
||||
)
|
||||
|
||||
func NewAPIV1Handler(
|
||||
ctx context.Context,
|
||||
router *mux.Router,
|
||||
accountManager account.Manager,
|
||||
networkMapController network_map.Controller,
|
||||
permissionsManager permissions.Manager) (http.Handler, error) {
|
||||
|
||||
v1validatorMiddleware, err := apiv1alpha1.CreateV1ApiValidatingMiddleware()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
router.Use(v1validatorMiddleware.Handler)
|
||||
|
||||
peersHandler := peers.NewHandler(accountManager, networkMapController, permissionsManager)
|
||||
_ = peersHandler.WithEndpointsForRouter(router)
|
||||
|
||||
usersHandler := users.NewHandler(accountManager)
|
||||
return usersHandler.WithEndpointsForRouter(router), nil
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
package peers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||
"github.com/netbirdio/netbird/management/server/account"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/management/server/api/v1alpha1/groups"
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/permissions"
|
||||
"github.com/netbirdio/netbird/shared/management/http/apiv1alpha1"
|
||||
"github.com/netbirdio/netbird/shared/management/http/util"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
accountManager account.Manager
|
||||
permissionsManager permissions.Manager
|
||||
networkMapController network_map.Controller
|
||||
}
|
||||
|
||||
// NewHandler creates a new peers Handler
|
||||
func NewHandler(accountManager account.Manager, networkMapController network_map.Controller, permissionsManager permissions.Manager) *Handler {
|
||||
return &Handler{
|
||||
accountManager: accountManager,
|
||||
networkMapController: networkMapController,
|
||||
permissionsManager: permissionsManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) WithEndpointsForRouter(router *mux.Router) *mux.Router {
|
||||
router.HandleFunc("/peers", h.GetAllPeers).Methods("GET", "OPTIONS")
|
||||
router.HandleFunc("/peers/{peerId}", h.HandlePeer).Methods("GET", "PUT", "DELETE", "OPTIONS")
|
||||
return router
|
||||
}
|
||||
|
||||
func (h *Handler) getPeer(ctx context.Context, accountID, peerID, userID string, w http.ResponseWriter) {
|
||||
peer, err := h.accountManager.GetPeer(ctx, accountID, peerID, userID)
|
||||
if err != nil {
|
||||
util.WriteError(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
if peer.ProxyMeta.Embedded {
|
||||
util.WriteError(ctx, status.Errorf(status.InvalidArgument, "not allowed to read peer"), w)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := h.accountManager.GetAccountSettings(ctx, accountID, activity.SystemInitiator)
|
||||
if err != nil {
|
||||
util.WriteError(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
dnsDomain := h.networkMapController.GetDNSDomain(settings)
|
||||
|
||||
grps, _ := h.accountManager.GetPeerGroups(ctx, accountID, peerID)
|
||||
grpsInfoMap := groups.ToGroupsInfoMap(grps, 0)
|
||||
|
||||
validPeers, invalidPeers, err := h.accountManager.GetValidatedPeers(ctx, accountID)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to list approved peers: %v", err)
|
||||
util.WriteError(ctx, fmt.Errorf("internal error"), w)
|
||||
return
|
||||
}
|
||||
|
||||
_, valid := validPeers[peer.ID]
|
||||
reason := invalidPeers[peer.ID]
|
||||
|
||||
util.WriteJSONObject(ctx, w, toSinglePeerResponse(peer, grpsInfoMap[peerID], dnsDomain, valid, reason))
|
||||
}
|
||||
|
||||
func (h *Handler) updatePeer(ctx context.Context, accountID, userID, peerID string, w http.ResponseWriter, r *http.Request) {
|
||||
req := &apiv1alpha1.PeerRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
||||
return
|
||||
}
|
||||
|
||||
update := &nbpeer.Peer{
|
||||
ID: peerID,
|
||||
SSHEnabled: req.SshEnabled,
|
||||
Name: req.Name,
|
||||
LoginExpirationEnabled: req.LoginExpirationEnabled,
|
||||
InactivityExpirationEnabled: req.InactivityExpirationEnabled,
|
||||
}
|
||||
|
||||
if req.ApprovalRequired != nil {
|
||||
// todo: looks like that we reset all status property, is it right?
|
||||
update.Status = &nbpeer.PeerStatus{
|
||||
RequiresApproval: *req.ApprovalRequired,
|
||||
}
|
||||
}
|
||||
|
||||
if req.Ip != nil {
|
||||
addr, err := netip.ParseAddr(*req.Ip)
|
||||
if err != nil {
|
||||
util.WriteError(ctx, status.Errorf(status.InvalidArgument, "invalid IP address %s: %v", *req.Ip, err), w)
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.accountManager.UpdatePeerIP(ctx, accountID, userID, peerID, addr); err != nil {
|
||||
util.WriteError(ctx, err, w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Ipv6 != nil {
|
||||
v6Addr, err := parseIPv6(req.Ipv6)
|
||||
if err != nil {
|
||||
util.WriteError(ctx, status.Errorf(status.InvalidArgument, "%v", err), w)
|
||||
return
|
||||
}
|
||||
if err = h.accountManager.UpdatePeerIPv6(ctx, accountID, userID, peerID, v6Addr); err != nil {
|
||||
util.WriteError(ctx, err, w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
peer, err := h.accountManager.UpdatePeer(ctx, accountID, userID, update)
|
||||
if err != nil {
|
||||
util.WriteError(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := h.accountManager.GetAccountSettings(ctx, accountID, activity.SystemInitiator)
|
||||
if err != nil {
|
||||
util.WriteError(ctx, err, w)
|
||||
return
|
||||
}
|
||||
dnsDomain := h.networkMapController.GetDNSDomain(settings)
|
||||
|
||||
peerGroups, err := h.accountManager.GetPeerGroups(ctx, accountID, peer.ID)
|
||||
if err != nil {
|
||||
util.WriteError(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
grpsInfoMap := groups.ToGroupsInfoMap(peerGroups, 0)
|
||||
|
||||
validPeers, invalidPeers, err := h.accountManager.GetValidatedPeers(ctx, accountID)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to get validated peers: %v", err)
|
||||
util.WriteError(ctx, fmt.Errorf("internal error"), w)
|
||||
return
|
||||
}
|
||||
|
||||
_, valid := validPeers[peer.ID]
|
||||
reason := invalidPeers[peer.ID]
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, toSinglePeerResponse(peer, grpsInfoMap[peerID], dnsDomain, valid, reason))
|
||||
}
|
||||
|
||||
func (h *Handler) deletePeer(ctx context.Context, accountID, userID string, peerID string, w http.ResponseWriter) {
|
||||
err := h.accountManager.DeletePeer(ctx, accountID, peerID, userID)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to delete peer: %v", err)
|
||||
util.WriteError(ctx, err, w)
|
||||
return
|
||||
}
|
||||
util.WriteJSONObject(ctx, w, util.EmptyObject{})
|
||||
}
|
||||
|
||||
// HandlePeer handles all peer requests for GET, PUT and DELETE operations
|
||||
func (h *Handler) HandlePeer(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
accountID, userID := userAuth.AccountId, userAuth.UserId
|
||||
vars := mux.Vars(r)
|
||||
peerID := vars["peerId"]
|
||||
if len(peerID) == 0 {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid peer ID"), w)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodDelete:
|
||||
h.deletePeer(r.Context(), accountID, userID, peerID, w)
|
||||
return
|
||||
case http.MethodGet:
|
||||
h.getPeer(r.Context(), accountID, peerID, userID, w)
|
||||
return
|
||||
case http.MethodPut:
|
||||
h.updatePeer(r.Context(), accountID, userID, peerID, w, r)
|
||||
return
|
||||
default:
|
||||
util.WriteError(r.Context(), status.Errorf(status.NotFound, "unknown METHOD"), w)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllPeers returns a list of all peers associated with a provided account
|
||||
func (h *Handler) GetAllPeers(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
nameFilter := r.URL.Query().Get("name")
|
||||
ipFilter := r.URL.Query().Get("ip")
|
||||
macFilter := r.URL.Query().Get("mac")
|
||||
|
||||
accountID, userID := userAuth.AccountId, userAuth.UserId
|
||||
|
||||
peers, err := h.accountManager.GetPeers(r.Context(), accountID, userID, nameFilter, ipFilter, macFilter)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := h.accountManager.GetAccountSettings(r.Context(), accountID, activity.SystemInitiator)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
dnsDomain := h.networkMapController.GetDNSDomain(settings)
|
||||
|
||||
grps, _ := h.accountManager.GetAllGroups(r.Context(), accountID, userID)
|
||||
|
||||
grpsInfoMap := groups.ToGroupsInfoMap(grps, len(peers))
|
||||
respBody := make([]*apiv1alpha1.PeerBatch, 0, len(peers))
|
||||
for _, peer := range peers {
|
||||
if peer.ProxyMeta.Embedded {
|
||||
continue
|
||||
}
|
||||
respBody = append(respBody, toPeerListItemResponse(peer, grpsInfoMap[peer.ID], dnsDomain, 0))
|
||||
}
|
||||
|
||||
validPeersMap, invalidPeersMap, err := h.accountManager.GetValidatedPeers(r.Context(), accountID)
|
||||
if err != nil {
|
||||
log.WithContext(r.Context()).Errorf("failed to get validated peers: %v", err)
|
||||
util.WriteError(r.Context(), fmt.Errorf("internal error"), w)
|
||||
return
|
||||
}
|
||||
h.setApprovalRequiredFlag(respBody, validPeersMap, invalidPeersMap)
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, respBody)
|
||||
}
|
||||
|
||||
func (h *Handler) setApprovalRequiredFlag(respBody []*apiv1alpha1.PeerBatch, validPeersMap map[string]struct{}, invalidPeersMap map[string]string) {
|
||||
for _, peer := range respBody {
|
||||
_, ok := validPeersMap[peer.Id]
|
||||
if !ok {
|
||||
peer.ApprovalRequired = true
|
||||
|
||||
reason := invalidPeersMap[peer.Id]
|
||||
peer.DisapprovalReason = &reason
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseIPv6(s *string) (netip.Addr, error) {
|
||||
if s == nil {
|
||||
return netip.Addr{}, fmt.Errorf("IPv6 address is nil")
|
||||
}
|
||||
addr, err := netip.ParseAddr(*s)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("invalid IPv6 address %s: %w", *s, err)
|
||||
}
|
||||
addr = addr.Unmap()
|
||||
if !addr.Is6() {
|
||||
return netip.Addr{}, fmt.Errorf("address %s is not IPv6", *s)
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []apiv1alpha1.GroupMinimum, dnsDomain string, approved bool, reason string) *apiv1alpha1.Peer {
|
||||
osVersion := peer.Meta.OSVersion
|
||||
if osVersion == "" {
|
||||
osVersion = peer.Meta.Core
|
||||
}
|
||||
|
||||
apiPeer := &apiv1alpha1.Peer{
|
||||
PeerMinimum: apiv1alpha1.PeerMinimum{
|
||||
Id: peer.ID,
|
||||
Name: peer.Name,
|
||||
},
|
||||
CreatedAt: peer.CreatedAt,
|
||||
Ip: peer.IP.String(),
|
||||
Ipv6: peerIPv6String(peer),
|
||||
ConnectionIp: peer.Location.ConnectionIP.String(),
|
||||
Connected: peer.Status.Connected,
|
||||
LastSeen: peer.Status.LastSeen,
|
||||
Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion),
|
||||
KernelVersion: peer.Meta.KernelVersion,
|
||||
GeonameId: int(peer.Location.GeoNameID),
|
||||
Version: peer.Meta.WtVersion,
|
||||
Groups: groupsInfo,
|
||||
SshEnabled: peer.SSHEnabled,
|
||||
Hostname: peer.Meta.Hostname,
|
||||
UserId: peer.UserID,
|
||||
UiVersion: peer.Meta.UIVersion,
|
||||
DnsLabel: fqdn(peer, dnsDomain),
|
||||
ExtraDnsLabels: fqdnList(peer.ExtraDNSLabels, dnsDomain),
|
||||
LoginExpirationEnabled: peer.LoginExpirationEnabled,
|
||||
LastLogin: peer.GetLastLogin(),
|
||||
LoginExpired: peer.Status.LoginExpired,
|
||||
ApprovalRequired: !approved,
|
||||
CountryCode: apiv1alpha1.CountryCode(peer.Location.CountryCode),
|
||||
CityName: apiv1alpha1.CityName(peer.Location.CityName),
|
||||
SerialNumber: peer.Meta.SystemSerialNumber,
|
||||
InactivityExpirationEnabled: peer.InactivityExpirationEnabled,
|
||||
Ephemeral: peer.Ephemeral,
|
||||
LocalFlags: &apiv1alpha1.PeerLocalFlags{
|
||||
BlockInbound: &peer.Meta.Flags.BlockInbound,
|
||||
BlockLanAccess: &peer.Meta.Flags.BlockLANAccess,
|
||||
DisableClientRoutes: &peer.Meta.Flags.DisableClientRoutes,
|
||||
DisableDns: &peer.Meta.Flags.DisableDNS,
|
||||
DisableFirewall: &peer.Meta.Flags.DisableFirewall,
|
||||
DisableServerRoutes: &peer.Meta.Flags.DisableServerRoutes,
|
||||
LazyConnectionEnabled: &peer.Meta.Flags.LazyConnectionEnabled,
|
||||
RosenpassEnabled: &peer.Meta.Flags.RosenpassEnabled,
|
||||
RosenpassPermissive: &peer.Meta.Flags.RosenpassPermissive,
|
||||
ServerSshAllowed: &peer.Meta.Flags.ServerSSHAllowed,
|
||||
RemoteJobsAllowed: &peer.Meta.Flags.RemoteJobsAllowed,
|
||||
},
|
||||
}
|
||||
|
||||
if !approved {
|
||||
apiPeer.DisapprovalReason = &reason
|
||||
}
|
||||
|
||||
return apiPeer
|
||||
}
|
||||
|
||||
func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []apiv1alpha1.GroupMinimum, dnsDomain string, accessiblePeersCount int) *apiv1alpha1.PeerBatch {
|
||||
osVersion := peer.Meta.OSVersion
|
||||
if osVersion == "" {
|
||||
osVersion = peer.Meta.Core
|
||||
}
|
||||
|
||||
return &apiv1alpha1.PeerBatch{
|
||||
CreatedAt: peer.CreatedAt,
|
||||
AccessiblePeersCount: accessiblePeersCount,
|
||||
Peer: apiv1alpha1.Peer{
|
||||
PeerMinimum: apiv1alpha1.PeerMinimum{
|
||||
Id: peer.ID,
|
||||
Name: peer.Name,
|
||||
},
|
||||
Ip: peer.IP.String(),
|
||||
Ipv6: peerIPv6String(peer),
|
||||
ConnectionIp: peer.Location.ConnectionIP.String(),
|
||||
Connected: peer.Status.Connected,
|
||||
LastSeen: peer.Status.LastSeen,
|
||||
Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion),
|
||||
KernelVersion: peer.Meta.KernelVersion,
|
||||
GeonameId: int(peer.Location.GeoNameID),
|
||||
Version: peer.Meta.WtVersion,
|
||||
Groups: groupsInfo,
|
||||
SshEnabled: peer.SSHEnabled,
|
||||
Hostname: peer.Meta.Hostname,
|
||||
UserId: peer.UserID,
|
||||
UiVersion: peer.Meta.UIVersion,
|
||||
DnsLabel: fqdn(peer, dnsDomain),
|
||||
ExtraDnsLabels: fqdnList(peer.ExtraDNSLabels, dnsDomain),
|
||||
LoginExpirationEnabled: peer.LoginExpirationEnabled,
|
||||
LastLogin: peer.GetLastLogin(),
|
||||
LoginExpired: peer.Status.LoginExpired,
|
||||
CountryCode: apiv1alpha1.CountryCode(peer.Location.CountryCode),
|
||||
CityName: apiv1alpha1.CityName(peer.Location.CityName),
|
||||
SerialNumber: peer.Meta.SystemSerialNumber,
|
||||
InactivityExpirationEnabled: peer.InactivityExpirationEnabled,
|
||||
Ephemeral: peer.Ephemeral,
|
||||
LocalFlags: &apiv1alpha1.PeerLocalFlags{
|
||||
BlockInbound: &peer.Meta.Flags.BlockInbound,
|
||||
BlockLanAccess: &peer.Meta.Flags.BlockLANAccess,
|
||||
DisableClientRoutes: &peer.Meta.Flags.DisableClientRoutes,
|
||||
DisableDns: &peer.Meta.Flags.DisableDNS,
|
||||
DisableFirewall: &peer.Meta.Flags.DisableFirewall,
|
||||
DisableServerRoutes: &peer.Meta.Flags.DisableServerRoutes,
|
||||
LazyConnectionEnabled: &peer.Meta.Flags.LazyConnectionEnabled,
|
||||
RosenpassEnabled: &peer.Meta.Flags.RosenpassEnabled,
|
||||
RosenpassPermissive: &peer.Meta.Flags.RosenpassPermissive,
|
||||
ServerSshAllowed: &peer.Meta.Flags.ServerSSHAllowed,
|
||||
RemoteJobsAllowed: &peer.Meta.Flags.RemoteJobsAllowed,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func fqdn(peer *nbpeer.Peer, dnsDomain string) string {
|
||||
fqdn := peer.FQDN(dnsDomain)
|
||||
if fqdn == "" {
|
||||
return peer.DNSLabel
|
||||
} else {
|
||||
return fqdn
|
||||
}
|
||||
}
|
||||
func fqdnList(extraLabels []string, dnsDomain string) []string {
|
||||
fqdnList := make([]string, 0, len(extraLabels))
|
||||
for _, label := range extraLabels {
|
||||
fqdn := fmt.Sprintf("%s.%s", label, dnsDomain)
|
||||
fqdnList = append(fqdnList, fqdn)
|
||||
}
|
||||
return fqdnList
|
||||
}
|
||||
|
||||
func peerIPv6String(peer *nbpeer.Peer) *string {
|
||||
if !peer.IPv6.IsValid() {
|
||||
return nil
|
||||
}
|
||||
s := peer.IPv6.String()
|
||||
return &s
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
package peers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/mock/gomock"
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/permissions"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/auth"
|
||||
"github.com/netbirdio/netbird/shared/management/http/apiv1alpha1"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||
)
|
||||
|
||||
const (
|
||||
testPeerID = "test_peer"
|
||||
noUpdateChannelTestPeerID = "no-update-channel"
|
||||
|
||||
adminUser = "admin_user"
|
||||
regularUser = "regular_user"
|
||||
serviceUser = "service_user"
|
||||
)
|
||||
|
||||
func initTestMetaData(t *testing.T, peers ...*nbpeer.Peer) *Handler {
|
||||
|
||||
peersMap := make(map[string]*nbpeer.Peer)
|
||||
for _, peer := range peers {
|
||||
peersMap[peer.ID] = peer.Copy()
|
||||
}
|
||||
|
||||
policy := &types.Policy{
|
||||
ID: "policy",
|
||||
AccountID: "test_id",
|
||||
Name: "policy",
|
||||
Enabled: true,
|
||||
Rules: []*types.PolicyRule{
|
||||
{
|
||||
ID: "rule",
|
||||
Name: "rule",
|
||||
Enabled: true,
|
||||
Action: "accept",
|
||||
Destinations: []string{"group1"},
|
||||
Sources: []string{"group1"},
|
||||
Bidirectional: true,
|
||||
Protocol: "all",
|
||||
Ports: []string{"80"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
srvUser := types.NewRegularUser(serviceUser, "", "")
|
||||
srvUser.IsServiceUser = true
|
||||
|
||||
account := &types.Account{
|
||||
Id: "test_id",
|
||||
Domain: "hotmail.com",
|
||||
Peers: peersMap,
|
||||
Users: map[string]*types.User{
|
||||
adminUser: types.NewAdminUser(adminUser),
|
||||
regularUser: types.NewRegularUser(regularUser, "", ""),
|
||||
serviceUser: srvUser,
|
||||
},
|
||||
Groups: map[string]*types.Group{
|
||||
"group1": {
|
||||
ID: "group1",
|
||||
AccountID: "test_id",
|
||||
Name: "group1",
|
||||
Issued: "api",
|
||||
Peers: maps.Keys(peersMap),
|
||||
},
|
||||
},
|
||||
Settings: &types.Settings{
|
||||
PeerLoginExpirationEnabled: true,
|
||||
PeerLoginExpiration: time.Hour,
|
||||
},
|
||||
Policies: []*types.Policy{policy},
|
||||
Network: &types.Network{
|
||||
Identifier: "ciclqisab2ss43jdn8q0",
|
||||
Net: net.IPNet{
|
||||
IP: net.ParseIP("100.67.0.0"),
|
||||
Mask: net.IPv4Mask(255, 255, 0, 0),
|
||||
},
|
||||
Serial: 51,
|
||||
},
|
||||
}
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
networkMapController := network_map.NewMockController(ctrl)
|
||||
networkMapController.EXPECT().
|
||||
GetDNSDomain(gomock.Any()).
|
||||
Return("domain").
|
||||
AnyTimes()
|
||||
|
||||
ctrl2 := gomock.NewController(t)
|
||||
permissionsManager := permissions.NewMockManager(ctrl2)
|
||||
permissionsManager.EXPECT().ValidateAccountAccess(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(context.Background(), nil).AnyTimes()
|
||||
permissionsManager.EXPECT().
|
||||
ValidateUserPermissions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Eq(modules.Peers), gomock.Eq(operations.Read)).
|
||||
DoAndReturn(func(ctx context.Context, accountID, userID string, module modules.Module, operation operations.Operation) (bool, context.Context, error) {
|
||||
user, ok := account.Users[userID]
|
||||
if !ok {
|
||||
return false, ctx, fmt.Errorf("user not found")
|
||||
}
|
||||
return user.HasAdminPower() || user.IsServiceUser, ctx, nil
|
||||
}).
|
||||
AnyTimes()
|
||||
|
||||
return &Handler{
|
||||
accountManager: &mock_server.MockAccountManager{
|
||||
UpdatePeerFunc: func(_ context.Context, accountID, userID string, update *nbpeer.Peer) (*nbpeer.Peer, error) {
|
||||
var p *nbpeer.Peer
|
||||
for _, peer := range peers {
|
||||
if update.ID == peer.ID {
|
||||
p = peer.Copy()
|
||||
break
|
||||
}
|
||||
}
|
||||
p.SSHEnabled = update.SSHEnabled
|
||||
p.LoginExpirationEnabled = update.LoginExpirationEnabled
|
||||
p.Name = update.Name
|
||||
return p, nil
|
||||
},
|
||||
UpdatePeerIPFunc: func(_ context.Context, accountID, userID, peerID string, newIP netip.Addr) error {
|
||||
for _, peer := range peers {
|
||||
if peer.ID == peerID {
|
||||
peer.IP = newIP
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("peer not found")
|
||||
},
|
||||
GetPeerFunc: func(_ context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) {
|
||||
var p *nbpeer.Peer
|
||||
for _, peer := range peers {
|
||||
if peerID == peer.ID {
|
||||
p = peer.Copy()
|
||||
break
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
},
|
||||
GetUserByIDFunc: func(ctx context.Context, id string) (*types.User, error) {
|
||||
switch id {
|
||||
case adminUser:
|
||||
return account.Users[adminUser], nil
|
||||
case regularUser:
|
||||
return account.Users[regularUser], nil
|
||||
case serviceUser:
|
||||
return account.Users[serviceUser], nil
|
||||
default:
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
},
|
||||
GetPeersFunc: func(_ context.Context, accountID, userID, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, error) {
|
||||
return peers, nil
|
||||
},
|
||||
GetPeerGroupsFunc: func(ctx context.Context, accountID, peerID string) ([]*types.Group, error) {
|
||||
peersID := make([]string, len(peers))
|
||||
for _, peer := range peers {
|
||||
peersID = append(peersID, peer.ID)
|
||||
}
|
||||
return []*types.Group{
|
||||
{
|
||||
ID: "group1",
|
||||
AccountID: accountID,
|
||||
Name: "group1",
|
||||
Issued: "api",
|
||||
Peers: peersID,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
GetDNSDomainFunc: func(settings *types.Settings) string {
|
||||
return "netbird.selfhosted"
|
||||
},
|
||||
GetAccountFunc: func(ctx context.Context, accountID string) (*types.Account, error) {
|
||||
return account, nil
|
||||
},
|
||||
GetAccountByIDFunc: func(ctx context.Context, accountID string, userID string) (*types.Account, error) {
|
||||
return account, nil
|
||||
},
|
||||
HasConnectedChannelFunc: func(peerID string) bool {
|
||||
statuses := make(map[string]struct{})
|
||||
for _, peer := range peers {
|
||||
if peer.ID == noUpdateChannelTestPeerID {
|
||||
break
|
||||
}
|
||||
statuses[peer.ID] = struct{}{}
|
||||
}
|
||||
_, ok := statuses[peerID]
|
||||
return ok
|
||||
},
|
||||
GetAccountSettingsFunc: func(ctx context.Context, accountID string, userID string) (*types.Settings, error) {
|
||||
return account.Settings, nil
|
||||
},
|
||||
},
|
||||
networkMapController: networkMapController,
|
||||
permissionsManager: permissionsManager,
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the GetAllPeers endpoint reachable in the route /api/peers
|
||||
// Use the metadata generated by initTestMetaData() to check for values
|
||||
func TestGetPeers(t *testing.T) {
|
||||
|
||||
peer := &nbpeer.Peer{
|
||||
ID: testPeerID,
|
||||
Key: "key",
|
||||
IP: netip.MustParseAddr("100.64.0.1"),
|
||||
IPv6: netip.MustParseAddr("fd00::1"),
|
||||
Status: &nbpeer.PeerStatus{Connected: true},
|
||||
Name: "PeerName",
|
||||
LoginExpirationEnabled: false,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
Hostname: "hostname",
|
||||
GoOS: "GoOS",
|
||||
Kernel: "kernel",
|
||||
Core: "core",
|
||||
Platform: "platform",
|
||||
OS: "OS",
|
||||
WtVersion: "development",
|
||||
SystemSerialNumber: "C02XJ0J0JGH7",
|
||||
},
|
||||
}
|
||||
|
||||
peer1 := peer.Copy()
|
||||
peer1.ID = noUpdateChannelTestPeerID
|
||||
|
||||
expectedUpdatedPeer := peer.Copy()
|
||||
expectedUpdatedPeer.LoginExpirationEnabled = true
|
||||
expectedUpdatedPeer.SSHEnabled = true
|
||||
expectedUpdatedPeer.Name = "New Name"
|
||||
|
||||
expectedPeer1 := peer1.Copy()
|
||||
expectedPeer1.Status.Connected = false
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
requestType string
|
||||
requestPath string
|
||||
contentType string
|
||||
requestBody io.Reader
|
||||
expectedArray bool
|
||||
expectedPeer *nbpeer.Peer
|
||||
}{
|
||||
{
|
||||
name: "GetPeersMetaData",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/peers",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedArray: true,
|
||||
expectedPeer: peer,
|
||||
},
|
||||
{
|
||||
name: "GetPeer with update channel",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/peers/" + testPeerID,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedArray: false,
|
||||
expectedPeer: peer,
|
||||
},
|
||||
{
|
||||
name: "PutPeer",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/peers/" + testPeerID,
|
||||
contentType: "application/json",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedArray: false,
|
||||
requestBody: bytes.NewBufferString("{\"login_expiration_enabled\":true,\"name\":\"New Name\",\"ssh_enabled\":true, \"inactivity_expiration_enabled\":true}"),
|
||||
expectedPeer: expectedUpdatedPeer,
|
||||
},
|
||||
}
|
||||
|
||||
p := initTestMetaData(t, peer, peer1)
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: "admin_user",
|
||||
Domain: "hotmail.com",
|
||||
AccountId: "test_id",
|
||||
})
|
||||
if tc.contentType != "" {
|
||||
req.Header.Set("Content-Type", tc.contentType)
|
||||
}
|
||||
|
||||
v1validator, err := apiv1alpha1.CreateV1ApiValidatingMiddleware()
|
||||
assert.NoError(t, err)
|
||||
router := mux.NewRouter()
|
||||
router.Use(v1validator.Handler)
|
||||
|
||||
router = p.WithEndpointsForRouter(router)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
if status := recorder.Code; status != tc.expectedStatus {
|
||||
t.Fatalf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("I don't know what I expected; %v", err)
|
||||
}
|
||||
|
||||
var got *apiv1alpha1.Peer
|
||||
if tc.expectedArray {
|
||||
respBody := []*apiv1alpha1.Peer{}
|
||||
err = json.Unmarshal(content, &respBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
// hardcode this check for now as we only have two peers in this suite
|
||||
assert.Equal(t, len(respBody), 2)
|
||||
|
||||
for _, peer := range respBody {
|
||||
if peer.Id == testPeerID {
|
||||
got = peer
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
got = &apiv1alpha1.Peer{}
|
||||
err = json.Unmarshal(content, got)
|
||||
if err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Log(got)
|
||||
|
||||
assert.Equal(t, tc.expectedPeer.Name, got.Name)
|
||||
assert.Equal(t, tc.expectedPeer.Meta.WtVersion, got.Version)
|
||||
assert.Equal(t, tc.expectedPeer.IP.String(), got.Ip)
|
||||
assert.Equal(t, "OS core", got.Os)
|
||||
assert.Equal(t, tc.expectedPeer.LoginExpirationEnabled, got.LoginExpirationEnabled)
|
||||
assert.Equal(t, tc.expectedPeer.SSHEnabled, got.SshEnabled)
|
||||
assert.Equal(t, tc.expectedPeer.Status.Connected, got.Connected)
|
||||
assert.Equal(t, tc.expectedPeer.Meta.SystemSerialNumber, got.SerialNumber)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeersHandlerUpdatePeerIP(t *testing.T) {
|
||||
testPeer := &nbpeer.Peer{
|
||||
ID: testPeerID,
|
||||
Key: "key",
|
||||
IP: netip.MustParseAddr("100.64.0.1"),
|
||||
IPv6: netip.MustParseAddr("fd00::1"),
|
||||
Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()},
|
||||
Name: "test-host@netbird.io",
|
||||
LoginExpirationEnabled: false,
|
||||
UserID: regularUser,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
Hostname: "test-host@netbird.io",
|
||||
Core: "22.04",
|
||||
},
|
||||
}
|
||||
|
||||
p := initTestMetaData(t, testPeer)
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
peerID string
|
||||
requestBody string
|
||||
callerUserID string
|
||||
expectedStatus int
|
||||
expectedIP string
|
||||
}{
|
||||
{
|
||||
name: "update peer IP successfully",
|
||||
peerID: testPeerID,
|
||||
requestBody: `{"name":"test", "ssh_enabled":true, "login_expiration_enabled":true, "inactivity_expiration_enabled":true, "ip": "100.64.0.100"}`,
|
||||
callerUserID: adminUser,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedIP: "100.64.0.100",
|
||||
},
|
||||
{
|
||||
name: "update peer IP with invalid IP",
|
||||
peerID: testPeerID,
|
||||
requestBody: `{"name":"test", "ssh_enabled":true, "login_expiration_enabled":true, "inactivity_expiration_enabled":true, "ip": "invalid-ip"}`,
|
||||
callerUserID: adminUser,
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/peers/%s", tc.peerID), bytes.NewBuffer([]byte(tc.requestBody)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: tc.callerUserID,
|
||||
Domain: "hotmail.com",
|
||||
AccountId: "test_id",
|
||||
})
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
v1validator, err := apiv1alpha1.CreateV1ApiValidatingMiddleware()
|
||||
assert.NoError(t, err)
|
||||
router := mux.NewRouter()
|
||||
router.Use(v1validator.Handler)
|
||||
|
||||
router = p.WithEndpointsForRouter(router)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, tc.expectedStatus, rr.Code)
|
||||
|
||||
if tc.expectedStatus == http.StatusOK && tc.expectedIP != "" {
|
||||
var updatedPeer apiv1alpha1.Peer
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &updatedPeer)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedIP, updatedPeer.Ip)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/account"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/management/http/apiv1alpha1"
|
||||
"github.com/netbirdio/netbird/shared/management/http/util"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
accountManager account.Manager
|
||||
}
|
||||
|
||||
func NewHandler(accountManager account.Manager) *Handler {
|
||||
return &Handler{
|
||||
accountManager: accountManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) WithEndpointsForRouter(router *mux.Router) *mux.Router {
|
||||
router.HandleFunc("/users", h.getAllUsers).Methods("GET", "OPTIONS")
|
||||
router.HandleFunc("/users", h.createUser).Methods("POST", "OPTIONS")
|
||||
router.HandleFunc("/users/{userId}", h.updateUser).Methods("PUT", "OPTIONS")
|
||||
router.HandleFunc("/users/{userId}", h.deleteUser).Methods("DELETE", "OPTIONS")
|
||||
return router
|
||||
}
|
||||
|
||||
// updateUser is a PUT requests to update User data
|
||||
func (h *Handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
util.WriteErrorResponse("wrong HTTP method", http.StatusMethodNotAllowed, w)
|
||||
return
|
||||
}
|
||||
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
accountID, userID := userAuth.AccountId, userAuth.UserId
|
||||
vars := mux.Vars(r)
|
||||
targetUserID := vars["userId"]
|
||||
if len(targetUserID) == 0 {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid user ID"), w)
|
||||
return
|
||||
}
|
||||
|
||||
existingUser, err := h.accountManager.GetUserByID(r.Context(), targetUserID)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
req := &apiv1alpha1.UserRequest{}
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
||||
return
|
||||
}
|
||||
|
||||
if req.AutoGroups == nil {
|
||||
util.WriteErrorResponse("auto_groups field can't be absent", http.StatusBadRequest, w)
|
||||
return
|
||||
}
|
||||
|
||||
userRole := types.StrRoleToUserRole(req.Role)
|
||||
if userRole == types.UserRoleUnknown {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid user role"), w)
|
||||
return
|
||||
}
|
||||
|
||||
newUser, err := h.accountManager.SaveUser(r.Context(), accountID, userID, &types.User{
|
||||
Id: targetUserID,
|
||||
Role: userRole,
|
||||
AutoGroups: req.AutoGroups,
|
||||
Blocked: req.IsBlocked,
|
||||
Issued: existingUser.Issued,
|
||||
IntegrationReference: existingUser.IntegrationReference,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, userID))
|
||||
}
|
||||
|
||||
// deleteUser is a DELETE request to delete a user
|
||||
func (h *Handler) deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
util.WriteErrorResponse("wrong HTTP method", http.StatusMethodNotAllowed, w)
|
||||
return
|
||||
}
|
||||
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
accountID, userID := userAuth.AccountId, userAuth.UserId
|
||||
vars := mux.Vars(r)
|
||||
targetUserID := vars["userId"]
|
||||
if len(targetUserID) == 0 {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid user ID"), w)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.accountManager.DeleteUser(r.Context(), accountID, userID, targetUserID)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, util.EmptyObject{})
|
||||
}
|
||||
|
||||
// createUser creates a User in the system with a status "invited" (effectively this is a user invite).
|
||||
func (h *Handler) createUser(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
util.WriteErrorResponse("wrong HTTP method", http.StatusMethodNotAllowed, w)
|
||||
return
|
||||
}
|
||||
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
accountID, userID := userAuth.AccountId, userAuth.UserId
|
||||
|
||||
req := &apiv1alpha1.UserCreateRequest{}
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
||||
return
|
||||
}
|
||||
|
||||
if types.StrRoleToUserRole(req.Role) == types.UserRoleUnknown {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "unknown user role %s", req.Role), w)
|
||||
return
|
||||
}
|
||||
|
||||
email := ""
|
||||
if req.Email != nil {
|
||||
email = *req.Email
|
||||
}
|
||||
|
||||
name := ""
|
||||
if req.Name != nil {
|
||||
name = *req.Name
|
||||
}
|
||||
|
||||
newUser, err := h.accountManager.CreateUser(r.Context(), accountID, userID, &types.UserInfo{
|
||||
Email: email,
|
||||
Name: name,
|
||||
Role: req.Role,
|
||||
AutoGroups: req.AutoGroups,
|
||||
IsServiceUser: req.IsServiceUser,
|
||||
Issued: types.UserIssuedAPI,
|
||||
})
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, userID))
|
||||
}
|
||||
|
||||
// getAllUsers returns a list of users of the account this user belongs to.
|
||||
// It also gathers additional user data (like email and name) from the IDP manager.
|
||||
func (h *Handler) getAllUsers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
util.WriteErrorResponse("wrong HTTP method", http.StatusMethodNotAllowed, w)
|
||||
return
|
||||
}
|
||||
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
accountID, userID := userAuth.AccountId, userAuth.UserId
|
||||
data, err := h.accountManager.GetUsersFromAccount(r.Context(), accountID, userID)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
serviceUser := r.URL.Query().Get("service_user")
|
||||
|
||||
users := make([]*apiv1alpha1.User, 0)
|
||||
for _, d := range data {
|
||||
if d.NonDeletable {
|
||||
continue
|
||||
}
|
||||
if serviceUser == "" {
|
||||
users = append(users, toUserResponse(d, userID))
|
||||
continue
|
||||
}
|
||||
|
||||
includeServiceUser, err := strconv.ParseBool(serviceUser)
|
||||
log.WithContext(r.Context()).Tracef("Should include service user: %v", includeServiceUser)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid service_user query parameter"), w)
|
||||
return
|
||||
}
|
||||
if includeServiceUser == d.IsServiceUser {
|
||||
users = append(users, toUserResponse(d, userID))
|
||||
}
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, users)
|
||||
}
|
||||
|
||||
func toUserResponse(user *types.UserInfo, currenUserID string) *apiv1alpha1.User {
|
||||
autoGroups := user.AutoGroups
|
||||
if autoGroups == nil {
|
||||
autoGroups = []string{}
|
||||
}
|
||||
|
||||
var userStatus apiv1alpha1.UserStatus
|
||||
switch user.Status {
|
||||
case "active":
|
||||
userStatus = apiv1alpha1.UserStatusActive
|
||||
case "invited":
|
||||
userStatus = apiv1alpha1.UserStatusInvited
|
||||
default:
|
||||
userStatus = apiv1alpha1.UserStatusBlocked
|
||||
}
|
||||
|
||||
if user.IsBlocked {
|
||||
userStatus = apiv1alpha1.UserStatusBlocked
|
||||
}
|
||||
|
||||
isCurrent := user.ID == currenUserID
|
||||
|
||||
var password *string
|
||||
if user.Password != "" {
|
||||
password = &user.Password
|
||||
}
|
||||
|
||||
var idpID *string
|
||||
if user.IdPID != "" {
|
||||
idpID = &user.IdPID
|
||||
}
|
||||
|
||||
return &apiv1alpha1.User{
|
||||
Id: user.ID,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
AutoGroups: autoGroups,
|
||||
Status: userStatus,
|
||||
IsCurrent: &isCurrent,
|
||||
IsServiceUser: &user.IsServiceUser,
|
||||
IsBlocked: user.IsBlocked,
|
||||
LastLogin: &user.LastLogin,
|
||||
Issued: &user.Issued,
|
||||
PendingApproval: user.PendingApproval,
|
||||
Password: password,
|
||||
IdpId: idpID,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/auth"
|
||||
"github.com/netbirdio/netbird/shared/management/http/apiv1alpha1"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
)
|
||||
|
||||
const (
|
||||
existingAccountID = "existingAccountID"
|
||||
notFoundAccountID = "notFoundAccountID"
|
||||
testDomain = "hotmail.com"
|
||||
existingUserID = "existingUserID"
|
||||
notFoundUserID = "notFoundUserID"
|
||||
serviceUserID = "serviceUserID"
|
||||
nonDeletableServiceUserID = "nonDeletableServiceUserID"
|
||||
regularUserID = "regularUserID"
|
||||
)
|
||||
|
||||
var usersTestAccount = &types.Account{
|
||||
Id: existingAccountID,
|
||||
Domain: testDomain,
|
||||
Users: map[string]*types.User{
|
||||
existingUserID: {
|
||||
Id: existingUserID,
|
||||
Role: "admin",
|
||||
IsServiceUser: false,
|
||||
AutoGroups: []string{"group_1"},
|
||||
Issued: types.UserIssuedAPI,
|
||||
},
|
||||
regularUserID: {
|
||||
Id: regularUserID,
|
||||
Role: "user",
|
||||
IsServiceUser: false,
|
||||
AutoGroups: []string{"group_1"},
|
||||
Issued: types.UserIssuedAPI,
|
||||
},
|
||||
serviceUserID: {
|
||||
Id: serviceUserID,
|
||||
Role: "user",
|
||||
IsServiceUser: true,
|
||||
AutoGroups: []string{"group_1"},
|
||||
Issued: types.UserIssuedAPI,
|
||||
},
|
||||
nonDeletableServiceUserID: {
|
||||
Id: nonDeletableServiceUserID,
|
||||
Role: "admin",
|
||||
IsServiceUser: true,
|
||||
NonDeletable: true,
|
||||
Issued: types.UserIssuedIntegration,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func initUsersTestData() *Handler {
|
||||
return &Handler{
|
||||
accountManager: &mock_server.MockAccountManager{
|
||||
GetUserByIDFunc: func(ctx context.Context, id string) (*types.User, error) {
|
||||
return usersTestAccount.Users[id], nil
|
||||
},
|
||||
GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) (map[string]*types.UserInfo, error) {
|
||||
usersInfos := make(map[string]*types.UserInfo)
|
||||
for _, v := range usersTestAccount.Users {
|
||||
usersInfos[v.Id] = &types.UserInfo{
|
||||
ID: v.Id,
|
||||
Role: string(v.Role),
|
||||
Name: "",
|
||||
Email: "",
|
||||
IsServiceUser: v.IsServiceUser,
|
||||
NonDeletable: v.NonDeletable,
|
||||
Issued: v.Issued,
|
||||
}
|
||||
}
|
||||
return usersInfos, nil
|
||||
},
|
||||
CreateUserFunc: func(_ context.Context, accountID, userID string, key *types.UserInfo) (*types.UserInfo, error) {
|
||||
if userID != existingUserID {
|
||||
return nil, status.Errorf(status.NotFound, "user with ID %s does not exists", userID)
|
||||
}
|
||||
return key, nil
|
||||
},
|
||||
DeleteUserFunc: func(_ context.Context, accountID string, initiatorUserID string, targetUserID string) error {
|
||||
if targetUserID == notFoundUserID {
|
||||
return status.Errorf(status.NotFound, "user with ID %s does not exists", targetUserID)
|
||||
}
|
||||
if !usersTestAccount.Users[targetUserID].IsServiceUser {
|
||||
return status.Errorf(status.PermissionDenied, "user with ID %s is not a service user and can not be deleted", targetUserID)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
SaveUserFunc: func(_ context.Context, accountID, userID string, update *types.User) (*types.UserInfo, error) {
|
||||
if update.Id == notFoundUserID {
|
||||
return nil, status.Errorf(status.NotFound, "user with ID %s does not exists", update.Id)
|
||||
}
|
||||
|
||||
if userID != existingUserID {
|
||||
return nil, status.Errorf(status.NotFound, "user with ID %s does not exists", userID)
|
||||
}
|
||||
|
||||
info, err := update.Copy().ToUserInfo(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return info, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUsers(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
requestType string
|
||||
requestPath string
|
||||
expectedUserIDs []string
|
||||
}{
|
||||
{name: "getAllUsers", requestType: http.MethodGet, requestPath: "/users", expectedStatus: http.StatusOK, expectedUserIDs: []string{existingUserID, regularUserID, serviceUserID}},
|
||||
{name: "GetOnlyServiceUsers", requestType: http.MethodGet, requestPath: "/users?service_user=true", expectedStatus: http.StatusOK, expectedUserIDs: []string{serviceUserID}},
|
||||
{name: "GetOnlyRegularUsers", requestType: http.MethodGet, requestPath: "/users?service_user=false", expectedStatus: http.StatusOK, expectedUserIDs: []string{existingUserID, regularUserID}},
|
||||
}
|
||||
|
||||
userHandler := initUsersTestData()
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: existingUserID,
|
||||
Domain: testDomain,
|
||||
AccountId: existingAccountID,
|
||||
})
|
||||
|
||||
v1validator, err := apiv1alpha1.CreateV1ApiValidatingMiddleware()
|
||||
assert.NoError(t, err)
|
||||
router := mux.NewRouter()
|
||||
router.Use(v1validator.Handler)
|
||||
|
||||
router = userHandler.WithEndpointsForRouter(router)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
content, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("I don't know what I expected; %v", err)
|
||||
}
|
||||
|
||||
if status := recorder.Code; status != tc.expectedStatus {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v, content: %s",
|
||||
status, tc.expectedStatus, string(content))
|
||||
return
|
||||
}
|
||||
|
||||
respBody := []*types.UserInfo{}
|
||||
err = json.Unmarshal(content, &respBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(respBody), len(tc.expectedUserIDs))
|
||||
for _, v := range respBody {
|
||||
assert.Contains(t, tc.expectedUserIDs, v.ID)
|
||||
assert.Equal(t, v.ID, usersTestAccount.Users[v.ID].Id)
|
||||
assert.Equal(t, v.Role, string(usersTestAccount.Users[v.ID].Role))
|
||||
assert.Equal(t, v.IsServiceUser, usersTestAccount.Users[v.ID].IsServiceUser)
|
||||
assert.Equal(t, v.Issued, usersTestAccount.Users[v.ID].Issued)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUser(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatusCode int
|
||||
requestType string
|
||||
requestPath string
|
||||
requestBody io.Reader
|
||||
expectedUserID string
|
||||
expectedRole string
|
||||
expectedStatus string
|
||||
expectedBlocked bool
|
||||
expectedIsServiceUser bool
|
||||
expectedGroups []string
|
||||
}{
|
||||
{
|
||||
name: "Update_Block_User",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/users/" + regularUserID,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedUserID: regularUserID,
|
||||
expectedBlocked: true,
|
||||
expectedRole: "user",
|
||||
expectedStatus: "blocked",
|
||||
expectedGroups: []string{"group_1"},
|
||||
requestBody: bytes.NewBufferString("{\"role\":\"user\",\"auto_groups\":[\"group_1\"],\"is_service_user\":false, \"is_blocked\": true}"),
|
||||
},
|
||||
{
|
||||
name: "Update_Change_Role_To_Admin",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/users/" + regularUserID,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedUserID: regularUserID,
|
||||
expectedBlocked: false,
|
||||
expectedRole: "admin",
|
||||
expectedStatus: "blocked",
|
||||
expectedGroups: []string{"group_1"},
|
||||
requestBody: bytes.NewBufferString("{\"role\":\"admin\",\"auto_groups\":[\"group_1\"],\"is_service_user\":false, \"is_blocked\": false}"),
|
||||
},
|
||||
{
|
||||
name: "Update_Groups",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/users/" + regularUserID,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedUserID: regularUserID,
|
||||
expectedBlocked: false,
|
||||
expectedRole: "admin",
|
||||
expectedStatus: "blocked",
|
||||
expectedGroups: []string{"group_2", "group_3"},
|
||||
requestBody: bytes.NewBufferString("{\"role\":\"admin\",\"auto_groups\":[\"group_3\", \"group_2\"],\"is_service_user\":false, \"is_blocked\": false}"),
|
||||
},
|
||||
{
|
||||
name: "Should_Fail_Because_AutoGroups_Is_Absent",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/users/" + regularUserID,
|
||||
expectedStatusCode: http.StatusUnprocessableEntity,
|
||||
expectedUserID: regularUserID,
|
||||
expectedBlocked: false,
|
||||
expectedRole: "admin",
|
||||
expectedStatus: "blocked",
|
||||
expectedGroups: []string{"group_2", "group_3"},
|
||||
requestBody: bytes.NewBufferString("{\"role\":\"admin\",\"is_service_user\":false, \"is_blocked\": false}"),
|
||||
},
|
||||
}
|
||||
|
||||
userHandler := initUsersTestData()
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: existingUserID,
|
||||
Domain: testDomain,
|
||||
AccountId: existingAccountID,
|
||||
})
|
||||
|
||||
v1validator, err := apiv1alpha1.CreateV1ApiValidatingMiddleware()
|
||||
assert.NoError(t, err)
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.Use(v1validator.Handler)
|
||||
|
||||
router = userHandler.WithEndpointsForRouter(router)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
if status := recorder.Code; status != tc.expectedStatusCode {
|
||||
t.Fatalf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
|
||||
if tc.expectedStatusCode == 200 {
|
||||
|
||||
content, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("I don't know what I expected; %v", err)
|
||||
}
|
||||
|
||||
respBody := &apiv1alpha1.User{}
|
||||
err = json.Unmarshal(content, &respBody)
|
||||
if err != nil {
|
||||
t.Fatalf("response content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.expectedUserID, respBody.Id)
|
||||
assert.Equal(t, tc.expectedRole, respBody.Role)
|
||||
assert.Equal(t, tc.expectedIsServiceUser, *respBody.IsServiceUser)
|
||||
assert.Equal(t, tc.expectedBlocked, respBody.IsBlocked)
|
||||
assert.Len(t, respBody.AutoGroups, len(tc.expectedGroups))
|
||||
|
||||
for _, expectedGroup := range tc.expectedGroups {
|
||||
exists := false
|
||||
for _, actualGroup := range respBody.AutoGroups {
|
||||
if expectedGroup == actualGroup {
|
||||
exists = true
|
||||
}
|
||||
}
|
||||
assert.True(t, exists, fmt.Sprintf("group %s not found in the response", expectedGroup))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateUser(t *testing.T) {
|
||||
name := "name"
|
||||
email := "email"
|
||||
serviceUserToAdd := apiv1alpha1.UserCreateRequest{
|
||||
AutoGroups: []string{},
|
||||
Email: nil,
|
||||
IsServiceUser: true,
|
||||
Name: &name,
|
||||
Role: "admin",
|
||||
}
|
||||
serviceUserString, err := json.Marshal(serviceUserToAdd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
regularUserToAdd := apiv1alpha1.UserCreateRequest{
|
||||
AutoGroups: []string{},
|
||||
Email: &email,
|
||||
IsServiceUser: true,
|
||||
Name: &name,
|
||||
Role: "admin",
|
||||
}
|
||||
regularUserString, err := json.Marshal(regularUserToAdd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
requestType string
|
||||
requestPath string
|
||||
requestBody io.Reader
|
||||
expectedResult []*types.User
|
||||
}{
|
||||
{name: "CreateServiceUser", requestType: http.MethodPost, requestPath: "/users", expectedStatus: http.StatusOK, requestBody: bytes.NewBuffer(serviceUserString)},
|
||||
// right now creation is blocked in AC middleware, will be refactored in the future
|
||||
{name: "CreateRegularUser", requestType: http.MethodPost, requestPath: "/users", expectedStatus: http.StatusOK, requestBody: bytes.NewBuffer(regularUserString)},
|
||||
}
|
||||
|
||||
userHandler := initUsersTestData()
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
|
||||
rr := httptest.NewRecorder()
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: existingUserID,
|
||||
Domain: testDomain,
|
||||
AccountId: existingAccountID,
|
||||
})
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
v1validator, err := apiv1alpha1.CreateV1ApiValidatingMiddleware()
|
||||
assert.NoError(t, err)
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.Use(v1validator.Handler)
|
||||
|
||||
router = userHandler.WithEndpointsForRouter(router)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
res := rr.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
if status := rr.Code; status != tc.expectedStatus {
|
||||
t.Fatalf("handler returned wrong status code: got %v want %v",
|
||||
status, tc.expectedStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteUser(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
expectedBody bool
|
||||
requestType string
|
||||
requestPath string
|
||||
requestVars map[string]string
|
||||
requestBody io.Reader
|
||||
}{
|
||||
{
|
||||
name: "Delete Regular User",
|
||||
requestType: http.MethodDelete,
|
||||
requestPath: "/users/" + regularUserID,
|
||||
requestVars: map[string]string{"userId": regularUserID},
|
||||
expectedStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "Delete Service User",
|
||||
requestType: http.MethodDelete,
|
||||
requestPath: "/users/" + serviceUserID,
|
||||
requestVars: map[string]string{"userId": serviceUserID},
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Delete Not Existing User",
|
||||
requestType: http.MethodDelete,
|
||||
requestPath: "/users/" + notFoundUserID,
|
||||
requestVars: map[string]string{"userId": notFoundUserID},
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
userHandler := initUsersTestData()
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
|
||||
req = mux.SetURLVars(req, tc.requestVars)
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: existingUserID,
|
||||
Domain: testDomain,
|
||||
AccountId: existingAccountID,
|
||||
})
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
v1validator, err := apiv1alpha1.CreateV1ApiValidatingMiddleware()
|
||||
assert.NoError(t, err)
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.Use(v1validator.Handler)
|
||||
router = userHandler.WithEndpointsForRouter(router)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
res := rr.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
if status := rr.Code; status != tc.expectedStatus {
|
||||
t.Fatalf("handler returned wrong status code: got %v want %v",
|
||||
status, tc.expectedStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/cors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain/manager"
|
||||
|
||||
@@ -36,7 +34,6 @@ import (
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/http/handlers/proxy"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/auth"
|
||||
"github.com/netbirdio/netbird/management/server/geolocation"
|
||||
nbgroups "github.com/netbirdio/netbird/management/server/groups"
|
||||
"github.com/netbirdio/netbird/management/server/http/handlers/accounts"
|
||||
@@ -51,18 +48,20 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/http/handlers/routes"
|
||||
"github.com/netbirdio/netbird/management/server/http/handlers/setup_keys"
|
||||
"github.com/netbirdio/netbird/management/server/http/handlers/users"
|
||||
"github.com/netbirdio/netbird/management/server/http/middleware"
|
||||
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
|
||||
nbinstance "github.com/netbirdio/netbird/management/server/instance"
|
||||
nbnetworks "github.com/netbirdio/netbird/management/server/networks"
|
||||
"github.com/netbirdio/netbird/management/server/networks/resources"
|
||||
"github.com/netbirdio/netbird/management/server/networks/routers"
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
"github.com/netbirdio/netbird/shared/ratelimit"
|
||||
)
|
||||
|
||||
// NewAPIHandler creates the Management service HTTP API handler registering all the available endpoints.
|
||||
func NewAPIHandler(ctx context.Context, router *mux.Router, accountManager account.Manager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation, authManager auth.Manager, appMetrics telemetry.AppMetrics, permissionsManager permissions.Manager, settingsManager settings.Manager, zManager zones.Manager, rManager records.Manager, networkMapController network_map.Controller, idpManager idpmanager.Manager, serviceManager service.Manager, reverseProxyDomainManager *manager.Manager, reverseProxyAccessLogsManager accesslogs.Manager, proxyGRPCServer *nbgrpc.ProxyServiceServer, trustedHTTPProxies []netip.Prefix, rateLimiter *ratelimit.APIRateLimiter, isValidChildAccount middleware.IsValidChildAccountFunc, agentNetworkManager agentnetwork.Manager, proxyTokenRevocationGuard proxytoken.RevocationGuard) (http.Handler, error) {
|
||||
func NewAPIHandler(ctx context.Context, router *mux.Router, accountManager account.Manager, networksManager nbnetworks.Manager,
|
||||
resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation,
|
||||
permissionsManager permissions.Manager, settingsManager settings.Manager, zManager zones.Manager, rManager records.Manager,
|
||||
networkMapController network_map.Controller, idpManager idpmanager.Manager, serviceManager service.Manager,
|
||||
reverseProxyDomainManager *manager.Manager, reverseProxyAccessLogsManager accesslogs.Manager, proxyGRPCServer *nbgrpc.ProxyServiceServer,
|
||||
trustedHTTPProxies []netip.Prefix, agentNetworkManager agentnetwork.Manager, proxyTokenRevocationGuard proxytoken.RevocationGuard) (http.Handler, error) {
|
||||
|
||||
// Register bypass paths for unauthenticated endpoints
|
||||
if err := bypass.AddBypassPath("/api/instance"); err != nil {
|
||||
@@ -83,28 +82,6 @@ func NewAPIHandler(ctx context.Context, router *mux.Router, accountManager accou
|
||||
return nil, fmt.Errorf("failed to add bypass path: %w", err)
|
||||
}
|
||||
|
||||
if rateLimiter == nil {
|
||||
log.Warn("NewAPIHandler: nil rate limiter, rate limiting disabled")
|
||||
rateLimiter = ratelimit.NewAPIRateLimiter(nil)
|
||||
rateLimiter.SetEnabled(false)
|
||||
}
|
||||
|
||||
authMiddleware := middleware.NewAuthMiddleware(
|
||||
authManager,
|
||||
accountManager.GetAccountIDFromUserAuth,
|
||||
accountManager.SyncUserJWTGroups,
|
||||
accountManager.GetUserFromUserAuth,
|
||||
rateLimiter,
|
||||
appMetrics.GetMeter(),
|
||||
isValidChildAccount,
|
||||
)
|
||||
|
||||
corsMiddleware := cors.AllowAll()
|
||||
|
||||
metricsMiddleware := appMetrics.HTTPMiddleware()
|
||||
|
||||
router.Use(metricsMiddleware.Handler, corsMiddleware.Handler, authMiddleware.Handler)
|
||||
|
||||
instanceManager, err := nbinstance.NewManager(ctx, accountManager.GetStore(), idpManager)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create instance manager: %w", err)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/netbirdio/netbird/management/server/account"
|
||||
"github.com/netbirdio/netbird/management/server/auth"
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
"github.com/netbirdio/netbird/shared/ratelimit"
|
||||
"github.com/rs/cors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func BuildMiddleware(rateLimiter *ratelimit.APIRateLimiter, authManager auth.Manager, accountManager account.Manager, metrics telemetry.AppMetrics, isValidChildAcctFunc IsValidChildAccountFunc) []mux.MiddlewareFunc {
|
||||
toret := make([]mux.MiddlewareFunc, 0)
|
||||
|
||||
toret = append(toret, metrics.HTTPMiddleware().Handler)
|
||||
toret = append(toret, cors.AllowAll().Handler)
|
||||
|
||||
if rateLimiter == nil {
|
||||
log.Warn("NewAPIHandler: nil rate limiter, rate limiting disabled")
|
||||
rateLimiter = ratelimit.NewAPIRateLimiter(nil)
|
||||
rateLimiter.SetEnabled(false)
|
||||
}
|
||||
toret = append(toret, NewAuthMiddleware(
|
||||
authManager,
|
||||
accountManager.GetAccountIDFromUserAuth,
|
||||
accountManager.SyncUserJWTGroups,
|
||||
accountManager.GetUserFromUserAuth,
|
||||
rateLimiter,
|
||||
metrics.GetMeter(),
|
||||
isValidChildAcctFunc,
|
||||
).Handler)
|
||||
|
||||
return toret
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/geolocation"
|
||||
"github.com/netbirdio/netbird/management/server/groups"
|
||||
http2 "github.com/netbirdio/netbird/management/server/http"
|
||||
"github.com/netbirdio/netbird/management/server/http/middleware"
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
|
||||
"github.com/netbirdio/netbird/management/server/networks"
|
||||
"github.com/netbirdio/netbird/management/server/networks/resources"
|
||||
@@ -145,7 +146,10 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
|
||||
zoneRecordsManager := recordsManager.NewManager(store, am, permissionsManager)
|
||||
|
||||
apiRouter := mux.NewRouter().PathPrefix("/api").Subrouter()
|
||||
apiHandler, err := http2.NewAPIHandler(ctx, apiRouter, am, networksManager, resourcesManager, routersManager, groupsManager, geoMock, authManagerMock, metrics, permissionsManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
apiRouter.Use(middleware.BuildMiddleware(nil, authManagerMock, am, metrics, nil)...)
|
||||
apiHandler, err := http2.NewAPIHandler(ctx, apiRouter, am, networksManager, resourcesManager, routersManager,
|
||||
groupsManager, geoMock, permissionsManager, settingsManager, customZonesManager, zoneRecordsManager,
|
||||
networkMapController, nil, serviceManager, nil, nil, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create API handler: %v", err)
|
||||
}
|
||||
@@ -284,7 +288,10 @@ func BuildApiBlackBoxWithDBStateAndPeerChannel(t testing_tools.TB, sqlFile strin
|
||||
zoneRecordsManager := recordsManager.NewManager(store, am, permissionsManager)
|
||||
|
||||
apiRouter := mux.NewRouter().PathPrefix("/api").Subrouter()
|
||||
apiHandler, err := http2.NewAPIHandler(ctx, apiRouter, am, networksManager, resourcesManager, routersManager, groupsManager, geoMock, authManagerMock, metrics, permissionsManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
apiRouter.Use(middleware.BuildMiddleware(nil, authManagerMock, am, metrics, nil)...)
|
||||
apiHandler, err := http2.NewAPIHandler(ctx, apiRouter, am, networksManager, resourcesManager, routersManager, groupsManager,
|
||||
geoMock, permissionsManager, settingsManager, customZonesManager, zoneRecordsManager,
|
||||
networkMapController, nil, serviceManager, nil, nil, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create API handler: %v", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,997 @@
|
||||
openapi: 3.1.0
|
||||
servers:
|
||||
- url: /api/v1alpha1
|
||||
description: Default server
|
||||
info:
|
||||
title: NetBird REST API
|
||||
description: API to manipulate groups, rules, policies and retrieve information about peers and users
|
||||
version: 1.0.0-alpha1
|
||||
tags:
|
||||
- name: Users
|
||||
description: Interact with and view information about users.
|
||||
- name: Tokens
|
||||
description: Interact with and view information about tokens.
|
||||
- name: Peers
|
||||
description: Interact with and view information about peers.
|
||||
- name: Setup Keys
|
||||
description: Interact with and view information about setup keys.
|
||||
- name: Groups
|
||||
description: Interact with and view information about groups.
|
||||
- name: Policies
|
||||
description: Interact with and view information about policies.
|
||||
- name: Posture Checks
|
||||
description: Interact with and view information about posture checks.
|
||||
- name: Routes
|
||||
description: Interact with and view information about routes.
|
||||
- name: DNS
|
||||
description: Interact with and view information about DNS configuration.
|
||||
- name: DNS Zones
|
||||
description: Interact with and view information about custom DNS zones.
|
||||
- name: Events
|
||||
description: View information about the account and network events.
|
||||
- name: Accounts
|
||||
description: View information about the accounts.
|
||||
- name: Ingress Ports
|
||||
description: Interact with and view information about the ingress peers and ports.
|
||||
x-cloud-only: true
|
||||
- name: Identity Providers
|
||||
description: Interact with and view information about identity providers.
|
||||
- name: Services
|
||||
description: Interact with and view information about reverse proxy services.
|
||||
- name: Instance
|
||||
description: Instance setup and status endpoints for initial configuration.
|
||||
- name: Jobs
|
||||
description: Interact with and view information about remote jobs.
|
||||
x-experimental: true
|
||||
- name: Usage
|
||||
description: Retrieve current usage statistics for the account.
|
||||
x-cloud-only: true
|
||||
- name: Subscription
|
||||
description: Manage and view information about account subscriptions.
|
||||
x-cloud-only: true
|
||||
- name: Plans
|
||||
description: Retrieve available plans and products.
|
||||
x-cloud-only: true
|
||||
- name: Checkout
|
||||
description: Manage checkout sessions for plan subscriptions.
|
||||
x-cloud-only: true
|
||||
- name: AWS Marketplace
|
||||
description: Manage AWS Marketplace subscriptions.
|
||||
x-cloud-only: true
|
||||
- name: Portal
|
||||
description: Access customer portal for subscription management.
|
||||
x-cloud-only: true
|
||||
- name: Invoice
|
||||
description: Manage and retrieve account invoices.
|
||||
x-cloud-only: true
|
||||
- name: MSP
|
||||
description: MSP portal for Tenant management.
|
||||
x-cloud-only: true
|
||||
- name: IDP SCIM Integrations
|
||||
description: Manage generic SCIM identity provider integrations for user and group sync.
|
||||
x-cloud-only: true
|
||||
- name: IDP Google Integrations
|
||||
description: Manage Google Workspace identity provider integrations for user and group sync.
|
||||
x-cloud-only: true
|
||||
- name: IDP Azure Integrations
|
||||
description: Manage Azure AD identity provider integrations for user and group sync.
|
||||
x-cloud-only: true
|
||||
- name: IDP Okta SCIM Integrations
|
||||
description: Manage Okta SCIM identity provider integrations for user and group sync.
|
||||
x-cloud-only: true
|
||||
- name: EDR Intune Integrations
|
||||
description: Manage Microsoft Intune EDR integrations.
|
||||
x-cloud-only: true
|
||||
- name: EDR SentinelOne Integrations
|
||||
description: Manage SentinelOne EDR integrations.
|
||||
x-cloud-only: true
|
||||
- name: EDR Falcon Integrations
|
||||
description: Manage CrowdStrike Falcon EDR integrations.
|
||||
x-cloud-only: true
|
||||
- name: EDR Huntress Integrations
|
||||
description: Manage Huntress EDR integrations.
|
||||
x-cloud-only: true
|
||||
- name: EDR FleetDM Integrations
|
||||
description: Manage FleetDM EDR integrations.
|
||||
x-cloud-only: true
|
||||
- name: EDR Peers
|
||||
description: Manage EDR compliance bypass for peers.
|
||||
x-cloud-only: true
|
||||
- name: Event Streaming Integrations
|
||||
description: Manage event streaming integrations.
|
||||
x-cloud-only: true
|
||||
- name: Notifications
|
||||
description: Manage notification channels for account event alerts.
|
||||
x-cloud-only: true
|
||||
components:
|
||||
schemas:
|
||||
CountryCode:
|
||||
description: 2-letter ISO 3166-1 alpha-2 code that represents the country
|
||||
type: string
|
||||
example: "DE"
|
||||
CityName:
|
||||
description: Commonly used English name of the city
|
||||
type: string
|
||||
example: "Berlin"
|
||||
Country:
|
||||
description: Describe country geographical location information
|
||||
type: object
|
||||
properties:
|
||||
country_name:
|
||||
description: Commonly used English name of the country
|
||||
type: string
|
||||
example: "Germany"
|
||||
country_code:
|
||||
$ref: '#/components/schemas/CountryCode'
|
||||
required:
|
||||
- country_name
|
||||
- country_code
|
||||
City:
|
||||
description: Describe city geographical location information
|
||||
type: object
|
||||
properties:
|
||||
geoname_id:
|
||||
description: Integer ID of the record in GeoNames database
|
||||
type: integer
|
||||
example: 2950158
|
||||
city_name:
|
||||
description: Commonly used English name of the city
|
||||
type: string
|
||||
example: "Berlin"
|
||||
required:
|
||||
- geoname_id
|
||||
- city_name
|
||||
ErrorResponse:
|
||||
type: object
|
||||
description: Standard error response
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
description: A human-readable error message.
|
||||
example: "couldn't parse JSON request"
|
||||
PeerBatch:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Peer'
|
||||
- type: object
|
||||
properties:
|
||||
created_at:
|
||||
description: Peer creation date (UTC)
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2023-05-05T09:00:35.477782Z"
|
||||
accessible_peers_count:
|
||||
description: Number of accessible peers
|
||||
type: integer
|
||||
example: 5
|
||||
required:
|
||||
- created_at
|
||||
- accessible_peers_count
|
||||
PeerMinimum:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: Peer ID
|
||||
type: string
|
||||
example: chacbco6lnnbn6cg5s90
|
||||
name:
|
||||
description: Peer's hostname
|
||||
type: string
|
||||
example: stage-host-1
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
GroupMinimum:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: Group ID
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7m0
|
||||
name:
|
||||
description: Group Name identifier
|
||||
type: string
|
||||
example: devs
|
||||
peers_count:
|
||||
description: Count of peers associated to the group
|
||||
type: integer
|
||||
example: 2
|
||||
resources_count:
|
||||
description: Count of resources associated to the group
|
||||
type: integer
|
||||
example: 5
|
||||
issued:
|
||||
description: How the group was issued (api, integration, jwt)
|
||||
type: string
|
||||
enum:
|
||||
- "api"
|
||||
- "integration"
|
||||
- "jwt"
|
||||
example: api
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- peers_count
|
||||
- resources_count
|
||||
PeerLocalFlags:
|
||||
type: object
|
||||
properties:
|
||||
rosenpass_enabled:
|
||||
description: Indicates whether Rosenpass is enabled on this peer
|
||||
type: boolean
|
||||
example: true
|
||||
rosenpass_permissive:
|
||||
description: Indicates whether Rosenpass is in permissive mode or not
|
||||
type: boolean
|
||||
example: false
|
||||
server_ssh_allowed:
|
||||
description: Indicates whether SSH access this peer is allowed or not
|
||||
type: boolean
|
||||
example: true
|
||||
remote_jobs_allowed:
|
||||
description: Indicates whether the peer has opted into management-requested remote jobs (e.g. debug bundles)
|
||||
type: boolean
|
||||
example: true
|
||||
disable_client_routes:
|
||||
description: Indicates whether client routes are disabled on this peer or not
|
||||
type: boolean
|
||||
example: false
|
||||
disable_server_routes:
|
||||
description: Indicates whether server routes are disabled on this peer or not
|
||||
type: boolean
|
||||
example: false
|
||||
disable_dns:
|
||||
description: Indicates whether DNS management is disabled on this peer or not
|
||||
type: boolean
|
||||
example: false
|
||||
disable_firewall:
|
||||
description: Indicates whether firewall management is disabled on this peer or not
|
||||
type: boolean
|
||||
example: false
|
||||
block_lan_access:
|
||||
description: Indicates whether LAN access is blocked on this peer when used as a routing peer
|
||||
type: boolean
|
||||
example: false
|
||||
block_inbound:
|
||||
description: Indicates whether inbound traffic is blocked on this peer
|
||||
type: boolean
|
||||
example: false
|
||||
lazy_connection_enabled:
|
||||
description: Indicates whether lazy connection is enabled on this peer
|
||||
type: boolean
|
||||
example: false
|
||||
Peer:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PeerMinimum'
|
||||
- type: object
|
||||
properties:
|
||||
created_at:
|
||||
description: Peer creation date (UTC)
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2023-05-05T09:00:35.477782Z"
|
||||
ip:
|
||||
description: Peer's IP address
|
||||
type: string
|
||||
example: 10.64.0.1
|
||||
ipv6:
|
||||
description: Peer's IPv6 overlay address
|
||||
type: string
|
||||
format: ipv6
|
||||
example: "fd00:4e42:ab12::1"
|
||||
connection_ip:
|
||||
description: Peer's public connection IP address
|
||||
type: string
|
||||
example: 35.64.0.1
|
||||
connected:
|
||||
description: Peer to Management connection status
|
||||
type: boolean
|
||||
example: true
|
||||
last_seen:
|
||||
description: Last time peer connected to Netbird's management service
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2023-05-05T10:05:26.420578Z"
|
||||
os:
|
||||
description: Peer's operating system and version
|
||||
type: string
|
||||
example: Darwin 13.2.1
|
||||
kernel_version:
|
||||
description: Peer's operating system kernel version
|
||||
type: string
|
||||
example: 23.2.0
|
||||
geoname_id:
|
||||
description: Unique identifier from the GeoNames database for a specific geographical location.
|
||||
type: integer
|
||||
example: 2643743
|
||||
version:
|
||||
description: Peer's daemon or cli version
|
||||
type: string
|
||||
example: 0.14.0
|
||||
groups:
|
||||
description: Groups that the peer belongs to
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/GroupMinimum'
|
||||
ssh_enabled:
|
||||
description: Indicates whether SSH server is enabled on this peer
|
||||
type: boolean
|
||||
example: true
|
||||
user_id:
|
||||
description: User ID of the user that enrolled this peer
|
||||
type: string
|
||||
example: google-oauth2|277474792786460067937
|
||||
hostname:
|
||||
description: Hostname of the machine
|
||||
type: string
|
||||
example: stage-host-1
|
||||
ui_version:
|
||||
description: Peer's desktop UI version
|
||||
type: string
|
||||
example: 0.14.0
|
||||
dns_label:
|
||||
description: Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
|
||||
type: string
|
||||
example: stage-host-1.netbird.cloud
|
||||
login_expiration_enabled:
|
||||
description: Indicates whether peer login expiration has been enabled or not
|
||||
type: boolean
|
||||
example: false
|
||||
login_expired:
|
||||
description: Indicates whether peer's login expired or not
|
||||
type: boolean
|
||||
example: false
|
||||
last_login:
|
||||
description: Last time this peer performed log in (authentication). E.g., user authenticated.
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2023-05-05T09:00:35.477782Z"
|
||||
inactivity_expiration_enabled:
|
||||
description: Indicates whether peer inactivity expiration has been enabled or not
|
||||
type: boolean
|
||||
example: false
|
||||
approval_required:
|
||||
description: (Cloud only) Indicates whether peer needs approval
|
||||
type: boolean
|
||||
example: true
|
||||
disapproval_reason:
|
||||
description: (Cloud only) Reason why the peer requires approval
|
||||
type: string
|
||||
country_code:
|
||||
$ref: '#/components/schemas/CountryCode'
|
||||
city_name:
|
||||
$ref: '#/components/schemas/CityName'
|
||||
serial_number:
|
||||
description: System serial number
|
||||
type: string
|
||||
example: "C02XJ0J0JGH7"
|
||||
extra_dns_labels:
|
||||
description: Extra DNS labels added to the peer
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: "stage-host-1"
|
||||
ephemeral:
|
||||
description: Indicates whether the peer is ephemeral or not
|
||||
type: boolean
|
||||
example: false
|
||||
local_flags:
|
||||
$ref: '#/components/schemas/PeerLocalFlags'
|
||||
required:
|
||||
- city_name
|
||||
- connected
|
||||
- connection_ip
|
||||
- country_code
|
||||
- created_at
|
||||
- dns_label
|
||||
- geoname_id
|
||||
- groups
|
||||
- hostname
|
||||
- ip
|
||||
- kernel_version
|
||||
- last_login
|
||||
- last_seen
|
||||
- login_expiration_enabled
|
||||
- login_expired
|
||||
- inactivity_expiration_enabled
|
||||
- os
|
||||
- ssh_enabled
|
||||
- user_id
|
||||
- version
|
||||
- ui_version
|
||||
- approval_required
|
||||
- serial_number
|
||||
- extra_dns_labels
|
||||
- ephemeral
|
||||
PeerRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: stage-host-1
|
||||
ssh_enabled:
|
||||
type: boolean
|
||||
example: true
|
||||
login_expiration_enabled:
|
||||
type: boolean
|
||||
example: false
|
||||
inactivity_expiration_enabled:
|
||||
type: boolean
|
||||
example: false
|
||||
approval_required:
|
||||
description: (Cloud only) Indicates whether peer needs approval
|
||||
type: boolean
|
||||
example: true
|
||||
ip:
|
||||
description: Peer's IP address
|
||||
type: string
|
||||
format: ipv4
|
||||
example: 100.64.0.15
|
||||
ipv6:
|
||||
description: Peer's IPv6 overlay address. Omitted if IPv6 is not enabled for the account.
|
||||
type: string
|
||||
format: ipv6
|
||||
example: "fd00:4e42:ab12::1"
|
||||
required:
|
||||
- name
|
||||
- ssh_enabled
|
||||
- login_expiration_enabled
|
||||
- inactivity_expiration_enabled
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: User ID
|
||||
type: string
|
||||
example: google-oauth2|277474792786460067937
|
||||
email:
|
||||
description: User's email address
|
||||
type: string
|
||||
example: demo@netbird.io
|
||||
password:
|
||||
description: User's password. Only present when user is created (create user endpoint is called) and only when IdP supports user creation with password.
|
||||
type: string
|
||||
example: super_secure_password
|
||||
name:
|
||||
description: User's name from idp provider
|
||||
type: string
|
||||
example: Tom Schulz
|
||||
role:
|
||||
description: User's NetBird account role
|
||||
type: string
|
||||
example: admin
|
||||
status:
|
||||
description: User's status
|
||||
type: string
|
||||
enum:
|
||||
- "active"
|
||||
- "invited"
|
||||
- "blocked"
|
||||
example: active
|
||||
last_login:
|
||||
description: Last time this user performed a login to the dashboard
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2023-05-05T09:00:35.477782Z"
|
||||
auto_groups:
|
||||
description: Group IDs to auto-assign to peers registered by this user
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7m0
|
||||
is_current:
|
||||
description: Is true if authenticated user is the same as this user
|
||||
type: boolean
|
||||
readOnly: true
|
||||
example: true
|
||||
is_service_user:
|
||||
description: Is true if this user is a service user
|
||||
type: boolean
|
||||
readOnly: true
|
||||
example: false
|
||||
is_blocked:
|
||||
description: Is true if this user is blocked. Blocked users can't use the system
|
||||
type: boolean
|
||||
example: false
|
||||
pending_approval:
|
||||
description: Is true if this user requires approval before being activated. Only applicable for users joining via domain matching when user_approval_required is enabled.
|
||||
type: boolean
|
||||
example: false
|
||||
issued:
|
||||
description: How user was issued by API or Integration
|
||||
type: string
|
||||
example: api
|
||||
idp_id:
|
||||
description: Identity provider ID (connector ID) that the user authenticated with. Only populated for users with Dex-encoded user IDs.
|
||||
type: string
|
||||
example: okta-abc123
|
||||
permissions:
|
||||
$ref: '#/components/schemas/UserPermissions'
|
||||
required:
|
||||
- id
|
||||
- email
|
||||
- name
|
||||
- role
|
||||
- auto_groups
|
||||
- status
|
||||
- is_blocked
|
||||
- pending_approval
|
||||
UserCreateRequest:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
description: User's Email to send invite to
|
||||
type: string
|
||||
example: demo@netbird.io
|
||||
name:
|
||||
description: User's full name
|
||||
type: string
|
||||
example: Tom Schulz
|
||||
role:
|
||||
description: User's NetBird account role
|
||||
type: string
|
||||
example: admin
|
||||
auto_groups:
|
||||
description: Group IDs to auto-assign to peers registered by this user
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7m0
|
||||
is_service_user:
|
||||
description: Is true if this user is a service user
|
||||
type: boolean
|
||||
example: false
|
||||
required:
|
||||
- role
|
||||
- auto_groups
|
||||
- is_service_user
|
||||
UserRequest:
|
||||
type: object
|
||||
properties:
|
||||
role:
|
||||
description: User's NetBird account role
|
||||
type: string
|
||||
example: admin
|
||||
auto_groups:
|
||||
description: Group IDs to auto-assign to peers registered by this user
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7m0
|
||||
is_blocked:
|
||||
description: If set to true then user is blocked and can't use the system
|
||||
type: boolean
|
||||
example: false
|
||||
required:
|
||||
- role
|
||||
- auto_groups
|
||||
- is_blocked
|
||||
UserPermissions:
|
||||
type: object
|
||||
properties:
|
||||
is_restricted:
|
||||
type: boolean
|
||||
description: Indicates whether this User's Peers view is restricted
|
||||
modules:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
propertyNames:
|
||||
type: string
|
||||
description: The operation type
|
||||
propertyNames:
|
||||
type: string
|
||||
description: The module name
|
||||
example: {"networks": {"read": true, "create": false, "update": false, "delete": false}, "peers": {"read": false, "create": false, "update": false, "delete": false}}
|
||||
required:
|
||||
- modules
|
||||
- is_restricted
|
||||
responses:
|
||||
not_found:
|
||||
description: Resource not found
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content: {}
|
||||
validation_failed_simple:
|
||||
description: Validation failed
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content: {}
|
||||
bad_request:
|
||||
description: Bad Request
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content: {}
|
||||
unprocessable:
|
||||
description: Unprocessable Entity
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
code:
|
||||
type: integer
|
||||
required:
|
||||
- message
|
||||
- code
|
||||
internal_error:
|
||||
description: Internal Server Error
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content: {}
|
||||
validation_failed:
|
||||
description: Validation failed
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content: {}
|
||||
forbidden:
|
||||
description: Forbidden
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content: {}
|
||||
requires_authentication:
|
||||
description: Requires authentication
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content: {}
|
||||
conflict:
|
||||
description: Conflict
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
headers:
|
||||
X-Request-Id:
|
||||
description: |
|
||||
Unique identifier assigned to the request by the server and set on every
|
||||
response. Useful for correlating client requests with server-side logs.
|
||||
schema:
|
||||
type: string
|
||||
example: cot7r4n3l3vh3qj4qveg
|
||||
example: cot7r4n3l3vh3qj4qveg
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
TokenAuth:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: Authorization
|
||||
description: >-
|
||||
Enter the token with the `Token` prefix, e.g. "Token nbp_F3f0d.....".
|
||||
pathItems:
|
||||
peers:
|
||||
get:
|
||||
summary: List all Peers
|
||||
description: Returns a list of all peers
|
||||
tags:
|
||||
- Peers
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
description: Page number
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
- name: page_size
|
||||
in: query
|
||||
description: Number of peers per page
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 250
|
||||
default: 100
|
||||
- name: connected
|
||||
in: query
|
||||
description: Filter by peer connected status
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
- name: approval_required
|
||||
in: query
|
||||
description: Filter by approval_required field
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
- name: os
|
||||
in: query
|
||||
description: Peer os
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- linux
|
||||
- windows
|
||||
- mac
|
||||
- android
|
||||
- ios
|
||||
- js
|
||||
- name: search
|
||||
in: query
|
||||
description: filter peers by name, dns_label, ip, os, version, serial_number, owner name, owner email, group names, mac
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- TokenAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: A JSON Array of Peers
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/PeerBatch'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'422':
|
||||
"$ref": "#/components/responses/unprocessable"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
peer:
|
||||
get:
|
||||
summary: Retrieve a Peer
|
||||
description: Get information about a peer
|
||||
tags:
|
||||
- Peers
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- TokenAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: peerId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a peer
|
||||
responses:
|
||||
'200':
|
||||
description: A Peer object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Peer'
|
||||
'400':
|
||||
"$ref": '#/components/responses/bad_request'
|
||||
'401':
|
||||
"$ref": '#/components/responses/requires_authentication'
|
||||
'403':
|
||||
"$ref": '#/components/responses/forbidden'
|
||||
'500':
|
||||
"$ref": '#/components/responses/internal_error'
|
||||
put:
|
||||
summary: Update a Peer
|
||||
description: Update information about a peer
|
||||
tags:
|
||||
- Peers
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- TokenAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: peerId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a peer
|
||||
requestBody:
|
||||
description: update a peer
|
||||
required: true
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/PeerRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A Peer object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Peer'
|
||||
'400':
|
||||
"$ref": '#/components/responses/bad_request'
|
||||
'401':
|
||||
"$ref": '#/components/responses/requires_authentication'
|
||||
'403':
|
||||
"$ref": '#/components/responses/forbidden'
|
||||
'422':
|
||||
"$ref": "#/components/responses/unprocessable"
|
||||
'500':
|
||||
"$ref": '#/components/responses/internal_error'
|
||||
delete:
|
||||
summary: Delete a Peer
|
||||
description: Delete a peer
|
||||
tags:
|
||||
- Peers
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- TokenAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: peerId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a peer
|
||||
responses:
|
||||
'200':
|
||||
description: Delete status code
|
||||
content: {}
|
||||
'400':
|
||||
"$ref": '#/components/responses/bad_request'
|
||||
'401':
|
||||
"$ref": '#/components/responses/requires_authentication'
|
||||
'403':
|
||||
"$ref": '#/components/responses/forbidden'
|
||||
'500':
|
||||
"$ref": '#/components/responses/internal_error'
|
||||
UsersPath:
|
||||
get:
|
||||
summary: List all Users
|
||||
description: Returns a list of all users
|
||||
tags:
|
||||
- Users
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- TokenAuth: []
|
||||
parameters:
|
||||
- in: query
|
||||
name: service_user
|
||||
schema:
|
||||
type: boolean
|
||||
description: Filters users and returns either regular users or service users
|
||||
responses:
|
||||
'200':
|
||||
description: A JSON array of Users
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'422':
|
||||
"$ref": "#/components/responses/unprocessable"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
post:
|
||||
summary: Create a User
|
||||
description: Creates a new service user or sends an invite to a regular user
|
||||
tags:
|
||||
- Users
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- TokenAuth: []
|
||||
requestBody:
|
||||
description: User invite information
|
||||
required: true
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserCreateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A User object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'422':
|
||||
"$ref": "#/components/responses/unprocessable"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
UserPath:
|
||||
put:
|
||||
summary: Update a User
|
||||
description: Update information about a User
|
||||
tags:
|
||||
- Users
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- TokenAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: userId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a user
|
||||
requestBody:
|
||||
description: User update
|
||||
required: true
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A User object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'422':
|
||||
"$ref": "#/components/responses/unprocessable"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
delete:
|
||||
summary: Delete a User
|
||||
description: This method removes a user from accessing the system. For this leaves the IDP user intact unless the `--user-delete-from-idp` is passed to management startup.
|
||||
tags:
|
||||
- Users
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- TokenAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: userId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a user
|
||||
responses:
|
||||
'200':
|
||||
description: Delete status code
|
||||
content: {}
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
paths:
|
||||
/peers:
|
||||
$ref: '#/components/pathItems/peers'
|
||||
/peers/{peerId}:
|
||||
$ref: '#/components/pathItems/peer'
|
||||
/users:
|
||||
$ref: '#/components/pathItems/UsersPath'
|
||||
/users/{userId}:
|
||||
$ref: '#/components/pathItems/UserPath'
|
||||
@@ -0,0 +1,31 @@
|
||||
components:
|
||||
schemas:
|
||||
GroupMinimum:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: Group ID
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7m0
|
||||
name:
|
||||
description: Group Name identifier
|
||||
type: string
|
||||
example: devs
|
||||
peers_count:
|
||||
description: Count of peers associated to the group
|
||||
type: integer
|
||||
example: 2
|
||||
resources_count:
|
||||
description: Count of resources associated to the group
|
||||
type: integer
|
||||
example: 5
|
||||
issued:
|
||||
description: How the group was issued (api, integration, jwt)
|
||||
type: string
|
||||
enum: [ "api", "integration", "jwt" ]
|
||||
example: api
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- peers_count
|
||||
- resources_count
|
||||
@@ -0,0 +1,250 @@
|
||||
openapi: 3.1.0
|
||||
servers:
|
||||
- url: /api/v1alpha1
|
||||
description: Default server
|
||||
info:
|
||||
title: NetBird REST API
|
||||
description: API to manipulate groups, rules, policies and retrieve information about peers and users
|
||||
version: 1.0.0-alpha1
|
||||
tags:
|
||||
- name: Users
|
||||
description: Interact with and view information about users.
|
||||
- name: Tokens
|
||||
description: Interact with and view information about tokens.
|
||||
- name: Peers
|
||||
description: Interact with and view information about peers.
|
||||
- name: Setup Keys
|
||||
description: Interact with and view information about setup keys.
|
||||
- name: Groups
|
||||
description: Interact with and view information about groups.
|
||||
- name: Policies
|
||||
description: Interact with and view information about policies.
|
||||
- name: Posture Checks
|
||||
description: Interact with and view information about posture checks.
|
||||
- name: Routes
|
||||
description: Interact with and view information about routes.
|
||||
- name: DNS
|
||||
description: Interact with and view information about DNS configuration.
|
||||
- name: DNS Zones
|
||||
description: Interact with and view information about custom DNS zones.
|
||||
- name: Events
|
||||
description: View information about the account and network events.
|
||||
- name: Accounts
|
||||
description: View information about the accounts.
|
||||
- name: Ingress Ports
|
||||
description: Interact with and view information about the ingress peers and ports.
|
||||
x-cloud-only: true
|
||||
- name: Identity Providers
|
||||
description: Interact with and view information about identity providers.
|
||||
- name: Services
|
||||
description: Interact with and view information about reverse proxy services.
|
||||
- name: Instance
|
||||
description: Instance setup and status endpoints for initial configuration.
|
||||
- name: Jobs
|
||||
description: Interact with and view information about remote jobs.
|
||||
x-experimental: true
|
||||
|
||||
- name: Usage
|
||||
description: Retrieve current usage statistics for the account.
|
||||
x-cloud-only: true
|
||||
- name: Subscription
|
||||
description: Manage and view information about account subscriptions.
|
||||
x-cloud-only: true
|
||||
- name: Plans
|
||||
description: Retrieve available plans and products.
|
||||
x-cloud-only: true
|
||||
- name: Checkout
|
||||
description: Manage checkout sessions for plan subscriptions.
|
||||
x-cloud-only: true
|
||||
- name: AWS Marketplace
|
||||
description: Manage AWS Marketplace subscriptions.
|
||||
x-cloud-only: true
|
||||
- name: Portal
|
||||
description: Access customer portal for subscription management.
|
||||
x-cloud-only: true
|
||||
- name: Invoice
|
||||
description: Manage and retrieve account invoices.
|
||||
x-cloud-only: true
|
||||
- name: MSP
|
||||
description: MSP portal for Tenant management.
|
||||
x-cloud-only: true
|
||||
- name: IDP SCIM Integrations
|
||||
description: Manage generic SCIM identity provider integrations for user and group sync.
|
||||
x-cloud-only: true
|
||||
- name: IDP Google Integrations
|
||||
description: Manage Google Workspace identity provider integrations for user and group sync.
|
||||
x-cloud-only: true
|
||||
- name: IDP Azure Integrations
|
||||
description: Manage Azure AD identity provider integrations for user and group sync.
|
||||
x-cloud-only: true
|
||||
- name: IDP Okta SCIM Integrations
|
||||
description: Manage Okta SCIM identity provider integrations for user and group sync.
|
||||
x-cloud-only: true
|
||||
- name: EDR Intune Integrations
|
||||
description: Manage Microsoft Intune EDR integrations.
|
||||
x-cloud-only: true
|
||||
- name: EDR SentinelOne Integrations
|
||||
description: Manage SentinelOne EDR integrations.
|
||||
x-cloud-only: true
|
||||
- name: EDR Falcon Integrations
|
||||
description: Manage CrowdStrike Falcon EDR integrations.
|
||||
x-cloud-only: true
|
||||
- name: EDR Huntress Integrations
|
||||
description: Manage Huntress EDR integrations.
|
||||
x-cloud-only: true
|
||||
- name: EDR FleetDM Integrations
|
||||
description: Manage FleetDM EDR integrations.
|
||||
x-cloud-only: true
|
||||
- name: EDR Peers
|
||||
description: Manage EDR compliance bypass for peers.
|
||||
x-cloud-only: true
|
||||
- name: Event Streaming Integrations
|
||||
description: Manage event streaming integrations.
|
||||
x-cloud-only: true
|
||||
- name: Notifications
|
||||
description: Manage notification channels for account event alerts.
|
||||
x-cloud-only: true
|
||||
|
||||
components:
|
||||
schemas:
|
||||
CountryCode:
|
||||
description: 2-letter ISO 3166-1 alpha-2 code that represents the country
|
||||
type: string
|
||||
example: "DE"
|
||||
CityName:
|
||||
description: Commonly used English name of the city
|
||||
type: string
|
||||
example: "Berlin"
|
||||
Country:
|
||||
description: Describe country geographical location information
|
||||
type: object
|
||||
properties:
|
||||
country_name:
|
||||
description: Commonly used English name of the country
|
||||
type: string
|
||||
example: "Germany"
|
||||
country_code:
|
||||
$ref: '#/components/schemas/CountryCode'
|
||||
required:
|
||||
- country_name
|
||||
- country_code
|
||||
City:
|
||||
description: Describe city geographical location information
|
||||
type: object
|
||||
properties:
|
||||
geoname_id:
|
||||
description: Integer ID of the record in GeoNames database
|
||||
type: integer
|
||||
example: 2950158
|
||||
city_name:
|
||||
description: Commonly used English name of the city
|
||||
type: string
|
||||
example: "Berlin"
|
||||
required:
|
||||
- geoname_id
|
||||
- city_name
|
||||
ErrorResponse:
|
||||
type: object
|
||||
description: Standard error response
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
description: A human-readable error message.
|
||||
example: "couldn't parse JSON request"
|
||||
responses:
|
||||
not_found:
|
||||
description: Resource not found
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content: { }
|
||||
validation_failed_simple:
|
||||
description: Validation failed
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content: { }
|
||||
bad_request:
|
||||
description: Bad Request
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content: { }
|
||||
unprocessable:
|
||||
description: Unprocessable Entity
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
code:
|
||||
type: integer
|
||||
required:
|
||||
- message
|
||||
- code
|
||||
internal_error:
|
||||
description: Internal Server Error
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content: { }
|
||||
validation_failed:
|
||||
description: Validation failed
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content: { }
|
||||
forbidden:
|
||||
description: Forbidden
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content: { }
|
||||
requires_authentication:
|
||||
description: Requires authentication
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content: { }
|
||||
conflict:
|
||||
description: Conflict
|
||||
headers:
|
||||
X-Request-Id:
|
||||
$ref: '#/components/headers/X-Request-Id'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
headers:
|
||||
X-Request-Id:
|
||||
description: |
|
||||
Unique identifier assigned to the request by the server and set on every
|
||||
response. Useful for correlating client requests with server-side logs.
|
||||
schema:
|
||||
type: string
|
||||
example: cot7r4n3l3vh3qj4qveg
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
TokenAuth:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: Authorization
|
||||
description: >-
|
||||
Enter the token with the `Token` prefix, e.g. "Token nbp_F3f0d.....".
|
||||
paths:
|
||||
/peers:
|
||||
$ref: './peer/peers.yaml'
|
||||
/peers/{peerId}:
|
||||
$ref: './peer/peer.yaml'
|
||||
/users:
|
||||
$ref: './user/user_paths.yaml#/UsersPath'
|
||||
/users/{userId}:
|
||||
$ref: './user/user_paths.yaml#/UserPath'
|
||||
@@ -0,0 +1,257 @@
|
||||
components:
|
||||
schemas:
|
||||
PeerRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: stage-host-1
|
||||
ssh_enabled:
|
||||
type: boolean
|
||||
example: true
|
||||
login_expiration_enabled:
|
||||
type: boolean
|
||||
example: false
|
||||
inactivity_expiration_enabled:
|
||||
type: boolean
|
||||
example: false
|
||||
approval_required:
|
||||
description: (Cloud only) Indicates whether peer needs approval
|
||||
type: boolean
|
||||
example: true
|
||||
ip:
|
||||
description: Peer's IP address
|
||||
type: string
|
||||
format: ipv4
|
||||
example: 100.64.0.15
|
||||
ipv6:
|
||||
description: Peer's IPv6 overlay address. Omitted if IPv6 is not enabled for the account.
|
||||
type: string
|
||||
format: ipv6
|
||||
example: "fd00:4e42:ab12::1"
|
||||
required:
|
||||
- name
|
||||
- ssh_enabled
|
||||
- login_expiration_enabled
|
||||
- inactivity_expiration_enabled
|
||||
PeerMinimum:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: Peer ID
|
||||
type: string
|
||||
example: chacbco6lnnbn6cg5s90
|
||||
name:
|
||||
description: Peer's hostname
|
||||
type: string
|
||||
example: stage-host-1
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
Peer:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PeerMinimum'
|
||||
- type: object
|
||||
properties:
|
||||
created_at:
|
||||
description: Peer creation date (UTC)
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2023-05-05T09:00:35.477782Z"
|
||||
ip:
|
||||
description: Peer's IP address
|
||||
type: string
|
||||
example: 10.64.0.1
|
||||
ipv6:
|
||||
description: Peer's IPv6 overlay address
|
||||
type: string
|
||||
format: ipv6
|
||||
example: "fd00:4e42:ab12::1"
|
||||
connection_ip:
|
||||
description: Peer's public connection IP address
|
||||
type: string
|
||||
example: 35.64.0.1
|
||||
connected:
|
||||
description: Peer to Management connection status
|
||||
type: boolean
|
||||
example: true
|
||||
last_seen:
|
||||
description: Last time peer connected to Netbird's management service
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2023-05-05T10:05:26.420578Z"
|
||||
os:
|
||||
description: Peer's operating system and version
|
||||
type: string
|
||||
example: Darwin 13.2.1
|
||||
kernel_version:
|
||||
description: Peer's operating system kernel version
|
||||
type: string
|
||||
example: 23.2.0
|
||||
geoname_id:
|
||||
description: Unique identifier from the GeoNames database for a specific geographical location.
|
||||
type: integer
|
||||
example: 2643743
|
||||
version:
|
||||
description: Peer's daemon or cli version
|
||||
type: string
|
||||
example: 0.14.0
|
||||
groups:
|
||||
description: Groups that the peer belongs to
|
||||
type: array
|
||||
items:
|
||||
$ref: '../group/components.yaml#/components/schemas/GroupMinimum'
|
||||
ssh_enabled:
|
||||
description: Indicates whether SSH server is enabled on this peer
|
||||
type: boolean
|
||||
example: true
|
||||
user_id:
|
||||
description: User ID of the user that enrolled this peer
|
||||
type: string
|
||||
example: google-oauth2|277474792786460067937
|
||||
hostname:
|
||||
description: Hostname of the machine
|
||||
type: string
|
||||
example: stage-host-1
|
||||
ui_version:
|
||||
description: Peer's desktop UI version
|
||||
type: string
|
||||
example: 0.14.0
|
||||
dns_label:
|
||||
description: Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
|
||||
type: string
|
||||
example: stage-host-1.netbird.cloud
|
||||
login_expiration_enabled:
|
||||
description: Indicates whether peer login expiration has been enabled or not
|
||||
type: boolean
|
||||
example: false
|
||||
login_expired:
|
||||
description: Indicates whether peer's login expired or not
|
||||
type: boolean
|
||||
example: false
|
||||
last_login:
|
||||
description: Last time this peer performed log in (authentication). E.g., user authenticated.
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2023-05-05T09:00:35.477782Z"
|
||||
inactivity_expiration_enabled:
|
||||
description: Indicates whether peer inactivity expiration has been enabled or not
|
||||
type: boolean
|
||||
example: false
|
||||
approval_required:
|
||||
description: (Cloud only) Indicates whether peer needs approval
|
||||
type: boolean
|
||||
example: true
|
||||
disapproval_reason:
|
||||
description: (Cloud only) Reason why the peer requires approval
|
||||
type: string
|
||||
country_code:
|
||||
$ref: '../openapi.yaml#/components/schemas/CountryCode'
|
||||
city_name:
|
||||
$ref: '../openapi.yaml#/components/schemas/CityName'
|
||||
serial_number:
|
||||
description: System serial number
|
||||
type: string
|
||||
example: "C02XJ0J0JGH7"
|
||||
extra_dns_labels:
|
||||
description: Extra DNS labels added to the peer
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: "stage-host-1"
|
||||
ephemeral:
|
||||
description: Indicates whether the peer is ephemeral or not
|
||||
type: boolean
|
||||
example: false
|
||||
local_flags:
|
||||
$ref: '#/components/schemas/PeerLocalFlags'
|
||||
required:
|
||||
- city_name
|
||||
- connected
|
||||
- connection_ip
|
||||
- country_code
|
||||
- created_at
|
||||
- dns_label
|
||||
- geoname_id
|
||||
- groups
|
||||
- hostname
|
||||
- ip
|
||||
- kernel_version
|
||||
- last_login
|
||||
- last_seen
|
||||
- login_expiration_enabled
|
||||
- login_expired
|
||||
- inactivity_expiration_enabled
|
||||
- os
|
||||
- ssh_enabled
|
||||
- user_id
|
||||
- version
|
||||
- ui_version
|
||||
- approval_required
|
||||
- serial_number
|
||||
- extra_dns_labels
|
||||
- ephemeral
|
||||
PeerLocalFlags:
|
||||
type: object
|
||||
properties:
|
||||
rosenpass_enabled:
|
||||
description: Indicates whether Rosenpass is enabled on this peer
|
||||
type: boolean
|
||||
example: true
|
||||
rosenpass_permissive:
|
||||
description: Indicates whether Rosenpass is in permissive mode or not
|
||||
type: boolean
|
||||
example: false
|
||||
server_ssh_allowed:
|
||||
description: Indicates whether SSH access this peer is allowed or not
|
||||
type: boolean
|
||||
example: true
|
||||
remote_jobs_allowed:
|
||||
description: Indicates whether the peer has opted into management-requested remote jobs (e.g. debug bundles)
|
||||
type: boolean
|
||||
example: true
|
||||
disable_client_routes:
|
||||
description: Indicates whether client routes are disabled on this peer or not
|
||||
type: boolean
|
||||
example: false
|
||||
disable_server_routes:
|
||||
description: Indicates whether server routes are disabled on this peer or not
|
||||
type: boolean
|
||||
example: false
|
||||
disable_dns:
|
||||
description: Indicates whether DNS management is disabled on this peer or not
|
||||
type: boolean
|
||||
example: false
|
||||
disable_firewall:
|
||||
description: Indicates whether firewall management is disabled on this peer or not
|
||||
type: boolean
|
||||
example: false
|
||||
block_lan_access:
|
||||
description: Indicates whether LAN access is blocked on this peer when used as a routing peer
|
||||
type: boolean
|
||||
example: false
|
||||
block_inbound:
|
||||
description: Indicates whether inbound traffic is blocked on this peer
|
||||
type: boolean
|
||||
example: false
|
||||
lazy_connection_enabled:
|
||||
description: Indicates whether lazy connection is enabled on this peer
|
||||
type: boolean
|
||||
example: false
|
||||
PeerBatch:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Peer'
|
||||
- type: object
|
||||
properties:
|
||||
created_at:
|
||||
description: Peer creation date (UTC)
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2023-05-05T09:00:35.477782Z"
|
||||
accessible_peers_count:
|
||||
description: Number of accessible peers
|
||||
type: integer
|
||||
example: 5
|
||||
required:
|
||||
- created_at
|
||||
- accessible_peers_count
|
||||
@@ -0,0 +1,93 @@
|
||||
get:
|
||||
summary: Retrieve a Peer
|
||||
description: Get information about a peer
|
||||
tags: [ Peers ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: peerId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a peer
|
||||
responses:
|
||||
'200':
|
||||
description: A Peer object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './components.yaml#/components/schemas/Peer'
|
||||
'400':
|
||||
"$ref": '../openapi.yaml#/components/responses/bad_request'
|
||||
'401':
|
||||
"$ref": '../openapi.yaml#/components/responses/requires_authentication'
|
||||
'403':
|
||||
"$ref": '../openapi.yaml#/components/responses/forbidden'
|
||||
'500':
|
||||
"$ref": '../openapi.yaml#/components/responses/internal_error'
|
||||
put:
|
||||
summary: Update a Peer
|
||||
description: Update information about a peer
|
||||
tags: [ Peers ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: peerId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a peer
|
||||
requestBody:
|
||||
description: update a peer
|
||||
required: true
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: './components.yaml#/components/schemas/PeerRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A Peer object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './components.yaml#/components/schemas/Peer'
|
||||
'400':
|
||||
"$ref": '../openapi.yaml#/components/responses/bad_request'
|
||||
'401':
|
||||
"$ref": '../openapi.yaml#/components/responses/requires_authentication'
|
||||
'403':
|
||||
"$ref": '../openapi.yaml#/components/responses/forbidden'
|
||||
'422':
|
||||
"$ref": "../openapi.yaml#/components/responses/unprocessable"
|
||||
'500':
|
||||
"$ref": '../openapi.yaml#/components/responses/internal_error'
|
||||
delete:
|
||||
summary: Delete a Peer
|
||||
description: Delete a peer
|
||||
tags: [ Peers ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: peerId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a peer
|
||||
responses:
|
||||
'200':
|
||||
description: Delete status code
|
||||
content: { }
|
||||
'400':
|
||||
"$ref": '../openapi.yaml#/components/responses/bad_request'
|
||||
'401':
|
||||
"$ref": '../openapi.yaml#/components/responses/requires_authentication'
|
||||
'403':
|
||||
"$ref": '../openapi.yaml#/components/responses/forbidden'
|
||||
'500':
|
||||
"$ref": '../openapi.yaml#/components/responses/internal_error'
|
||||
@@ -0,0 +1,77 @@
|
||||
get:
|
||||
summary: List all Peers
|
||||
description: Returns a list of all peers
|
||||
tags: [ Peers ]
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
description: Page number
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
- name: page_size
|
||||
in: query
|
||||
description: Number of peers per page
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 250
|
||||
default: 100
|
||||
- name: connected
|
||||
in: query
|
||||
description: Filter by peer connected status
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
- name: approval_required
|
||||
in: query
|
||||
description: Filter by approval_required field
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
- name: os
|
||||
in: query
|
||||
description: Peer os
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- linux
|
||||
- windows
|
||||
- mac
|
||||
- android
|
||||
- ios
|
||||
- js
|
||||
- name: search
|
||||
in: query
|
||||
description: filter peers by name, dns_label, ip, os, version, serial_number, owner name, owner email, group names, mac
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
responses:
|
||||
'200':
|
||||
description: A JSON Array of Peers
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: './components.yaml#/components/schemas/PeerBatch'
|
||||
'400':
|
||||
"$ref": "../openapi.yaml#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "../openapi.yaml#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "../openapi.yaml#/components/responses/forbidden"
|
||||
'422':
|
||||
"$ref": "../openapi.yaml#/components/responses/unprocessable"
|
||||
'500':
|
||||
"$ref": "../openapi.yaml#/components/responses/internal_error"
|
||||
@@ -0,0 +1,93 @@
|
||||
package apiv1alpha1
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/http/util"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
"github.com/pb33f/libopenapi"
|
||||
validator "github.com/pb33f/libopenapi-validator"
|
||||
"github.com/pb33f/libopenapi-validator/config"
|
||||
"github.com/pb33f/libopenapi-validator/errors"
|
||||
"github.com/pb33f/libopenapi/datamodel"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// TODO (dmitri) this needs to be extracted, as it will grow to 500Kb
|
||||
//
|
||||
//go:embed bundle.yaml
|
||||
var bundle []byte
|
||||
|
||||
func CreateV1ApiValidatingMiddleware() (*V1ValidatorMiddleware, error) {
|
||||
doc, err := libopenapi.NewDocumentWithConfiguration(bundle, &datamodel.DocumentConfiguration{
|
||||
Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
})),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
model, err := doc.BuildV3Model()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO figure out document validation: rn it's possible to have a spec
|
||||
// that's not entirely correct -- parts of it fail to parse, but silently
|
||||
v := validator.NewValidatorFromV3Model(&model.Model,
|
||||
config.WithoutSecurityValidation(),
|
||||
config.WithStandardBodyDecoders(),
|
||||
config.WithRejectUnsupportedBodyContent(),
|
||||
config.WithRequestDefaults())
|
||||
// v.SetDocument(doc)
|
||||
// v.ValidatePathParams()
|
||||
// if valid, errs := v.ValidateDocument(); !valid {
|
||||
// return nil, fmt.Errorf("error validating OpenAPI doc, %s", errs)
|
||||
// }
|
||||
|
||||
return &V1ValidatorMiddleware{Validator: v}, nil
|
||||
}
|
||||
|
||||
type V1ValidatorMiddleware struct {
|
||||
Validator validator.Validator
|
||||
}
|
||||
|
||||
func (v *V1ValidatorMiddleware) Handler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
valid, errs := v.Validator.ValidateHttpRequestSync(r)
|
||||
if !valid {
|
||||
validationErrs := make([]string, 0, len(errs))
|
||||
for _, err := range errs {
|
||||
validationErrs = append(validationErrs, validationError(err))
|
||||
}
|
||||
|
||||
log.WithContext(r.Context()).Debugf("error validating request: %s", strings.Join(validationErrs, ", "))
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid request: %s", strings.Join(validationErrs, ", ")), w)
|
||||
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func validationError(err *errors.ValidationError) string {
|
||||
if err.SchemaValidationErrors != nil {
|
||||
errs := make([]string, 0, len(err.SchemaValidationErrors))
|
||||
for _, e := range err.SchemaValidationErrors {
|
||||
errs = append(errs, fmt.Sprintf("field %s: %s", e.FieldPath, e.Reason))
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", err.Message, strings.Join(errs, ", "))
|
||||
} else {
|
||||
if err.SpecLine > 0 && err.SpecCol > 0 {
|
||||
return fmt.Sprintf("%s, Line: %d, Column: %d", err.Message, err.SpecLine, err.SpecCol)
|
||||
} else {
|
||||
return fmt.Sprint(err.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package apiv1alpha1
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/pb33f/libopenapi"
|
||||
"github.com/pb33f/libopenapi/bundler"
|
||||
"github.com/pb33f/libopenapi/datamodel"
|
||||
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
|
||||
"github.com/pb33f/libopenapi/generator/golang"
|
||||
)
|
||||
|
||||
var ApiPath = filepath.Join("shared", "management", "http", "apiv1alpha1", "openapi.yaml")
|
||||
|
||||
func GenerateV1ApiBindings(openapipath string) ([]byte, *v3.Document, error) {
|
||||
specFile, err := os.ReadFile(openapipath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
multiFileDoc, err := libopenapi.NewDocumentWithConfiguration(specFile, &datamodel.DocumentConfiguration{
|
||||
AllowFileReferences: true,
|
||||
BasePath: filepath.Dir(openapipath),
|
||||
SpecFilePath: openapipath,
|
||||
ExtractRefsSequentially: true,
|
||||
Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelError,
|
||||
})),
|
||||
TransformSiblingRefs: true, // enable openapi 3.1 compliance by default
|
||||
MergeReferencedProperties: true, // enable enhanced resolution by default
|
||||
PropertyMergeStrategy: datamodel.PreserveLocal, // local properties take precedence
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
multiFileModel, err := multiFileDoc.BuildV3Model()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
bundle, err := bundler.BundleDocumentComposed(&multiFileModel.Model, &bundler.BundleCompositionConfig{
|
||||
StrictValidation: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
doc, err := libopenapi.NewDocumentWithConfiguration(bundle, &datamodel.DocumentConfiguration{
|
||||
Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
})),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
model, err := doc.BuildV3Model()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return bundle, &model.Model, nil
|
||||
}
|
||||
|
||||
func GenerateV1Schema(model *v3.Document) (*golang.GeneratedFile, error) {
|
||||
// render every schema in components.schemas into one file
|
||||
gen := golang.NewGenerator(
|
||||
golang.WithGeneratedComment(true),
|
||||
golang.WithFormatMapping("date-time", "time.Time", "time"),
|
||||
golang.WithOptionalFieldsAsPointers(true),
|
||||
golang.WithEnumConstants(true),
|
||||
golang.WithNestedTypeNameDelimiter(""),
|
||||
golang.WithPackageName("apiv1alpha1"),
|
||||
golang.WithFieldNameResolver(toPublicName))
|
||||
|
||||
return gen.RenderSchemas(model.Components.Schemas)
|
||||
}
|
||||
|
||||
// this is to keep existing naming of fields like "id", "url", etc
|
||||
// libopenapi by default converts them to all-uppercase, like "ID", "URL", etc
|
||||
func toPublicName(name string) string {
|
||||
parts := splitIdentifier(name)
|
||||
if len(parts) == 0 {
|
||||
return "Value"
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, p := range parts {
|
||||
rs := []rune(strings.ToLower(p))
|
||||
rs[0] = unicode.ToUpper(rs[0])
|
||||
b.WriteString(string(rs))
|
||||
}
|
||||
out := b.String()
|
||||
first := []rune(out)[0]
|
||||
if unicode.IsDigit(first) {
|
||||
return "Value" + out
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func splitIdentifier(name string) []string {
|
||||
var raw []string
|
||||
var b strings.Builder
|
||||
flush := func() {
|
||||
if b.Len() > 0 {
|
||||
raw = append(raw, b.String())
|
||||
b.Reset()
|
||||
}
|
||||
}
|
||||
for _, r := range name {
|
||||
switch {
|
||||
case unicode.IsLetter(r) || unicode.IsDigit(r):
|
||||
b.WriteRune(r)
|
||||
default:
|
||||
flush()
|
||||
}
|
||||
}
|
||||
flush()
|
||||
var parts []string
|
||||
for _, part := range raw {
|
||||
parts = append(parts, splitCamel(part)...)
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func splitCamel(value string) []string {
|
||||
rs := []rune(value)
|
||||
if len(rs) == 0 {
|
||||
return nil
|
||||
}
|
||||
var parts []string
|
||||
start := 0
|
||||
for i := 1; i < len(rs); i++ {
|
||||
prev := rs[i-1]
|
||||
cur := rs[i]
|
||||
var next rune
|
||||
if i+1 < len(rs) {
|
||||
next = rs[i+1]
|
||||
}
|
||||
lowerToUpper := unicode.IsLower(prev) && unicode.IsUpper(cur)
|
||||
acronymToWord := unicode.IsUpper(prev) && unicode.IsUpper(cur) && next != 0 && unicode.IsLower(next)
|
||||
if lowerToUpper || acronymToWord {
|
||||
parts = append(parts, string(rs[start:i]))
|
||||
start = i
|
||||
}
|
||||
}
|
||||
parts = append(parts, string(rs[start:]))
|
||||
return parts
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
// Code generated by libopenapi generator/golang. DO NOT EDIT.
|
||||
|
||||
package apiv1alpha1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country.
|
||||
// CountryCode example value is defined in the OpenAPI schema.
|
||||
type CountryCode string
|
||||
|
||||
// CityName Commonly used English name of the city.
|
||||
// CityName example value is defined in the OpenAPI schema.
|
||||
type CityName string
|
||||
|
||||
// Country Describe country geographical location information.
|
||||
type Country struct {
|
||||
// CountryName Commonly used English name of the country.
|
||||
// CountryName example value is defined in the OpenAPI schema.
|
||||
CountryName string `json:"country_name"`
|
||||
CountryCode CountryCode `json:"country_code"`
|
||||
}
|
||||
|
||||
// City Describe city geographical location information.
|
||||
type City struct {
|
||||
// GeonameId Integer ID of the record in GeoNames database.
|
||||
// GeonameId example value is defined in the OpenAPI schema.
|
||||
GeonameId int `json:"geoname_id"`
|
||||
// CityName Commonly used English name of the city.
|
||||
// CityName example value is defined in the OpenAPI schema.
|
||||
CityName string `json:"city_name"`
|
||||
}
|
||||
|
||||
// ErrorResponse Standard error response.
|
||||
type ErrorResponse struct {
|
||||
// Message A human-readable error message.
|
||||
// Message example value is defined in the OpenAPI schema.
|
||||
Message *string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type PeerBatch struct {
|
||||
Peer
|
||||
// CreatedAt Peer creation date (UTC).
|
||||
// CreatedAt example value is defined in the OpenAPI schema.
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// AccessiblePeersCount Number of accessible peers.
|
||||
// AccessiblePeersCount example value is defined in the OpenAPI schema.
|
||||
AccessiblePeersCount int `json:"accessible_peers_count"`
|
||||
}
|
||||
|
||||
type PeerMinimum struct {
|
||||
// Id Peer ID.
|
||||
// Id example value is defined in the OpenAPI schema.
|
||||
Id string `json:"id"`
|
||||
// Name Peer's hostname.
|
||||
// Name example value is defined in the OpenAPI schema.
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// GroupMinimumIssued How the group was issued (api, integration, jwt).
|
||||
// GroupMinimumIssued example value is defined in the OpenAPI schema.
|
||||
type GroupMinimumIssued string
|
||||
|
||||
const (
|
||||
GroupMinimumIssuedAPI GroupMinimumIssued = "api"
|
||||
GroupMinimumIssuedIntegration GroupMinimumIssued = "integration"
|
||||
GroupMinimumIssuedJWT GroupMinimumIssued = "jwt"
|
||||
)
|
||||
|
||||
type GroupMinimum struct {
|
||||
// Id Group ID.
|
||||
// Id example value is defined in the OpenAPI schema.
|
||||
Id string `json:"id"`
|
||||
// Name Group Name identifier.
|
||||
// Name example value is defined in the OpenAPI schema.
|
||||
Name string `json:"name"`
|
||||
// PeersCount Count of peers associated to the group.
|
||||
// PeersCount example value is defined in the OpenAPI schema.
|
||||
PeersCount int `json:"peers_count"`
|
||||
// ResourcesCount Count of resources associated to the group.
|
||||
// ResourcesCount example value is defined in the OpenAPI schema.
|
||||
ResourcesCount int `json:"resources_count"`
|
||||
// Issued How the group was issued (api, integration, jwt).
|
||||
// Issued example value is defined in the OpenAPI schema.
|
||||
Issued *GroupMinimumIssued `json:"issued,omitempty"`
|
||||
}
|
||||
|
||||
type PeerLocalFlags struct {
|
||||
// RosenpassEnabled Indicates whether Rosenpass is enabled on this peer.
|
||||
// RosenpassEnabled example value is defined in the OpenAPI schema.
|
||||
RosenpassEnabled *bool `json:"rosenpass_enabled,omitempty"`
|
||||
// RosenpassPermissive Indicates whether Rosenpass is in permissive mode or not.
|
||||
// RosenpassPermissive example value is defined in the OpenAPI schema.
|
||||
RosenpassPermissive *bool `json:"rosenpass_permissive,omitempty"`
|
||||
// ServerSshAllowed Indicates whether SSH access this peer is allowed or not.
|
||||
// ServerSshAllowed example value is defined in the OpenAPI schema.
|
||||
ServerSshAllowed *bool `json:"server_ssh_allowed,omitempty"`
|
||||
// RemoteJobsAllowed Indicates whether the peer has opted into management-requested remote jobs (e.g. debug bundles).
|
||||
// RemoteJobsAllowed example value is defined in the OpenAPI schema.
|
||||
RemoteJobsAllowed *bool `json:"remote_jobs_allowed,omitempty"`
|
||||
// DisableClientRoutes Indicates whether client routes are disabled on this peer or not.
|
||||
// DisableClientRoutes example value is defined in the OpenAPI schema.
|
||||
DisableClientRoutes *bool `json:"disable_client_routes,omitempty"`
|
||||
// DisableServerRoutes Indicates whether server routes are disabled on this peer or not.
|
||||
// DisableServerRoutes example value is defined in the OpenAPI schema.
|
||||
DisableServerRoutes *bool `json:"disable_server_routes,omitempty"`
|
||||
// DisableDns Indicates whether DNS management is disabled on this peer or not.
|
||||
// DisableDns example value is defined in the OpenAPI schema.
|
||||
DisableDns *bool `json:"disable_dns,omitempty"`
|
||||
// DisableFirewall Indicates whether firewall management is disabled on this peer or not.
|
||||
// DisableFirewall example value is defined in the OpenAPI schema.
|
||||
DisableFirewall *bool `json:"disable_firewall,omitempty"`
|
||||
// BlockLanAccess Indicates whether LAN access is blocked on this peer when used as a routing peer.
|
||||
// BlockLanAccess example value is defined in the OpenAPI schema.
|
||||
BlockLanAccess *bool `json:"block_lan_access,omitempty"`
|
||||
// BlockInbound Indicates whether inbound traffic is blocked on this peer.
|
||||
// BlockInbound example value is defined in the OpenAPI schema.
|
||||
BlockInbound *bool `json:"block_inbound,omitempty"`
|
||||
// LazyConnectionEnabled Indicates whether lazy connection is enabled on this peer.
|
||||
// LazyConnectionEnabled example value is defined in the OpenAPI schema.
|
||||
LazyConnectionEnabled *bool `json:"lazy_connection_enabled,omitempty"`
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
PeerMinimum
|
||||
// CreatedAt Peer creation date (UTC).
|
||||
// CreatedAt example value is defined in the OpenAPI schema.
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// Ip Peer's IP address.
|
||||
// Ip example value is defined in the OpenAPI schema.
|
||||
Ip string `json:"ip"`
|
||||
// Ipv6 Peer's IPv6 overlay address.
|
||||
// Ipv6 example value is defined in the OpenAPI schema.
|
||||
Ipv6 *string `json:"ipv6,omitempty"`
|
||||
// ConnectionIp Peer's public connection IP address.
|
||||
// ConnectionIp example value is defined in the OpenAPI schema.
|
||||
ConnectionIp string `json:"connection_ip"`
|
||||
// Connected Peer to Management connection status.
|
||||
// Connected example value is defined in the OpenAPI schema.
|
||||
Connected bool `json:"connected"`
|
||||
// LastSeen Last time peer connected to Netbird's management service.
|
||||
// LastSeen example value is defined in the OpenAPI schema.
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
// Os Peer's operating system and version.
|
||||
// Os example value is defined in the OpenAPI schema.
|
||||
Os string `json:"os"`
|
||||
// KernelVersion Peer's operating system kernel version.
|
||||
// KernelVersion example value is defined in the OpenAPI schema.
|
||||
KernelVersion string `json:"kernel_version"`
|
||||
// GeonameId Unique identifier from the GeoNames database for a specific geographical location.
|
||||
// GeonameId example value is defined in the OpenAPI schema.
|
||||
GeonameId int `json:"geoname_id"`
|
||||
// Version Peer's daemon or cli version.
|
||||
// Version example value is defined in the OpenAPI schema.
|
||||
Version string `json:"version"`
|
||||
// Groups Groups that the peer belongs to.
|
||||
Groups []GroupMinimum `json:"groups"`
|
||||
// SshEnabled Indicates whether SSH server is enabled on this peer.
|
||||
// SshEnabled example value is defined in the OpenAPI schema.
|
||||
SshEnabled bool `json:"ssh_enabled"`
|
||||
// UserId User ID of the user that enrolled this peer.
|
||||
// UserId example value is defined in the OpenAPI schema.
|
||||
UserId string `json:"user_id"`
|
||||
// Hostname Hostname of the machine.
|
||||
// Hostname example value is defined in the OpenAPI schema.
|
||||
Hostname string `json:"hostname"`
|
||||
// UiVersion Peer's desktop UI version.
|
||||
// UiVersion example value is defined in the OpenAPI schema.
|
||||
UiVersion string `json:"ui_version"`
|
||||
// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud.
|
||||
// DnsLabel example value is defined in the OpenAPI schema.
|
||||
DnsLabel string `json:"dns_label"`
|
||||
// LoginExpirationEnabled Indicates whether peer login expiration has been enabled or not.
|
||||
// LoginExpirationEnabled example value is defined in the OpenAPI schema.
|
||||
LoginExpirationEnabled bool `json:"login_expiration_enabled"`
|
||||
// LoginExpired Indicates whether peer's login expired or not.
|
||||
// LoginExpired example value is defined in the OpenAPI schema.
|
||||
LoginExpired bool `json:"login_expired"`
|
||||
// LastLogin Last time this peer performed log in (authentication). E.g., user authenticated.
|
||||
// LastLogin example value is defined in the OpenAPI schema.
|
||||
LastLogin time.Time `json:"last_login"`
|
||||
// InactivityExpirationEnabled Indicates whether peer inactivity expiration has been enabled or not.
|
||||
// InactivityExpirationEnabled example value is defined in the OpenAPI schema.
|
||||
InactivityExpirationEnabled bool `json:"inactivity_expiration_enabled"`
|
||||
// ApprovalRequired (Cloud only) Indicates whether peer needs approval.
|
||||
// ApprovalRequired example value is defined in the OpenAPI schema.
|
||||
ApprovalRequired bool `json:"approval_required"`
|
||||
// DisapprovalReason (Cloud only) Reason why the peer requires approval.
|
||||
DisapprovalReason *string `json:"disapproval_reason,omitempty"`
|
||||
CountryCode CountryCode `json:"country_code"`
|
||||
CityName CityName `json:"city_name"`
|
||||
// SerialNumber System serial number.
|
||||
// SerialNumber example value is defined in the OpenAPI schema.
|
||||
SerialNumber string `json:"serial_number"`
|
||||
// ExtraDnsLabels Extra DNS labels added to the peer.
|
||||
ExtraDnsLabels []string `json:"extra_dns_labels"`
|
||||
// Ephemeral Indicates whether the peer is ephemeral or not.
|
||||
// Ephemeral example value is defined in the OpenAPI schema.
|
||||
Ephemeral bool `json:"ephemeral"`
|
||||
LocalFlags *PeerLocalFlags `json:"local_flags,omitempty"`
|
||||
}
|
||||
|
||||
type PeerRequest struct {
|
||||
// Name example value is defined in the OpenAPI schema.
|
||||
Name string `json:"name"`
|
||||
// SshEnabled example value is defined in the OpenAPI schema.
|
||||
SshEnabled bool `json:"ssh_enabled"`
|
||||
// LoginExpirationEnabled example value is defined in the OpenAPI schema.
|
||||
LoginExpirationEnabled bool `json:"login_expiration_enabled"`
|
||||
// InactivityExpirationEnabled example value is defined in the OpenAPI schema.
|
||||
InactivityExpirationEnabled bool `json:"inactivity_expiration_enabled"`
|
||||
// ApprovalRequired (Cloud only) Indicates whether peer needs approval.
|
||||
// ApprovalRequired example value is defined in the OpenAPI schema.
|
||||
ApprovalRequired *bool `json:"approval_required,omitempty"`
|
||||
// Ip Peer's IP address.
|
||||
// Ip example value is defined in the OpenAPI schema.
|
||||
Ip *string `json:"ip,omitempty"`
|
||||
// Ipv6 Peer's IPv6 overlay address. Omitted if IPv6 is not enabled for the account.
|
||||
// Ipv6 example value is defined in the OpenAPI schema.
|
||||
Ipv6 *string `json:"ipv6,omitempty"`
|
||||
}
|
||||
|
||||
// UserStatus User's status.
|
||||
// UserStatus example value is defined in the OpenAPI schema.
|
||||
type UserStatus string
|
||||
|
||||
const (
|
||||
UserStatusActive UserStatus = "active"
|
||||
UserStatusInvited UserStatus = "invited"
|
||||
UserStatusBlocked UserStatus = "blocked"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
// Id User ID.
|
||||
// Id example value is defined in the OpenAPI schema.
|
||||
Id string `json:"id"`
|
||||
// Email User's email address.
|
||||
// Email example value is defined in the OpenAPI schema.
|
||||
Email string `json:"email"`
|
||||
// Password User's password. Only present when user is created (create user endpoint is called) and only when IdP supports user creation with password.
|
||||
// Password example value is defined in the OpenAPI schema.
|
||||
Password *string `json:"password,omitempty"`
|
||||
// Name User's name from idp provider.
|
||||
// Name example value is defined in the OpenAPI schema.
|
||||
Name string `json:"name"`
|
||||
// Role User's NetBird account role.
|
||||
// Role example value is defined in the OpenAPI schema.
|
||||
Role string `json:"role"`
|
||||
// Status User's status.
|
||||
// Status example value is defined in the OpenAPI schema.
|
||||
Status UserStatus `json:"status"`
|
||||
// LastLogin Last time this user performed a login to the dashboard.
|
||||
// LastLogin example value is defined in the OpenAPI schema.
|
||||
LastLogin *time.Time `json:"last_login,omitempty"`
|
||||
// AutoGroups Group IDs to auto-assign to peers registered by this user.
|
||||
AutoGroups []string `json:"auto_groups"`
|
||||
// IsCurrent Is true if authenticated user is the same as this user.
|
||||
// IsCurrent readOnly.
|
||||
// IsCurrent example value is defined in the OpenAPI schema.
|
||||
IsCurrent *bool `json:"is_current,omitempty"`
|
||||
// IsServiceUser Is true if this user is a service user.
|
||||
// IsServiceUser readOnly.
|
||||
// IsServiceUser example value is defined in the OpenAPI schema.
|
||||
IsServiceUser *bool `json:"is_service_user,omitempty"`
|
||||
// IsBlocked Is true if this user is blocked. Blocked users can't use the system.
|
||||
// IsBlocked example value is defined in the OpenAPI schema.
|
||||
IsBlocked bool `json:"is_blocked"`
|
||||
// PendingApproval Is true if this user requires approval before being activated. Only applicable for users joining via domain matching when user_approval_required is enabled.
|
||||
// PendingApproval example value is defined in the OpenAPI schema.
|
||||
PendingApproval bool `json:"pending_approval"`
|
||||
// Issued How user was issued by API or Integration.
|
||||
// Issued example value is defined in the OpenAPI schema.
|
||||
Issued *string `json:"issued,omitempty"`
|
||||
// IdpId Identity provider ID (connector ID) that the user authenticated with. Only populated for users with Dex-encoded user IDs.
|
||||
// IdpId example value is defined in the OpenAPI schema.
|
||||
IdpId *string `json:"idp_id,omitempty"`
|
||||
Permissions *UserPermissions `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
type UserCreateRequest struct {
|
||||
// Email User's Email to send invite to.
|
||||
// Email example value is defined in the OpenAPI schema.
|
||||
Email *string `json:"email,omitempty"`
|
||||
// Name User's full name.
|
||||
// Name example value is defined in the OpenAPI schema.
|
||||
Name *string `json:"name,omitempty"`
|
||||
// Role User's NetBird account role.
|
||||
// Role example value is defined in the OpenAPI schema.
|
||||
Role string `json:"role"`
|
||||
// AutoGroups Group IDs to auto-assign to peers registered by this user.
|
||||
AutoGroups []string `json:"auto_groups"`
|
||||
// IsServiceUser Is true if this user is a service user.
|
||||
// IsServiceUser example value is defined in the OpenAPI schema.
|
||||
IsServiceUser bool `json:"is_service_user"`
|
||||
}
|
||||
|
||||
type UserRequest struct {
|
||||
// Role User's NetBird account role.
|
||||
// Role example value is defined in the OpenAPI schema.
|
||||
Role string `json:"role"`
|
||||
// AutoGroups Group IDs to auto-assign to peers registered by this user.
|
||||
AutoGroups []string `json:"auto_groups"`
|
||||
// IsBlocked If set to true then user is blocked and can't use the system.
|
||||
// IsBlocked example value is defined in the OpenAPI schema.
|
||||
IsBlocked bool `json:"is_blocked"`
|
||||
}
|
||||
|
||||
type UserPermissionsModulesAdditionalProperty struct {
|
||||
AdditionalProperties map[string]bool `json:"-"`
|
||||
}
|
||||
|
||||
func (m *UserPermissionsModulesAdditionalProperty) UnmarshalJSON(data []byte) error {
|
||||
type Alias UserPermissionsModulesAdditionalProperty
|
||||
var known Alias
|
||||
if err := json.Unmarshal(data, &known); err != nil {
|
||||
return err
|
||||
}
|
||||
*m = UserPermissionsModulesAdditionalProperty(known)
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
m.AdditionalProperties = make(map[string]bool, len(raw))
|
||||
for key, value := range raw {
|
||||
var decoded bool
|
||||
if err := json.Unmarshal(value, &decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
m.AdditionalProperties[key] = decoded
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m UserPermissionsModulesAdditionalProperty) MarshalJSON() ([]byte, error) {
|
||||
type Alias UserPermissionsModulesAdditionalProperty
|
||||
encoded, err := json.Marshal(Alias(m))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var object map[string]json.RawMessage
|
||||
if err := json.Unmarshal(encoded, &object); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, value := range m.AdditionalProperties {
|
||||
encodedValue, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
object[key] = encodedValue
|
||||
}
|
||||
return json.Marshal(object)
|
||||
}
|
||||
|
||||
// UserPermissionsModules example value is defined in the OpenAPI schema.
|
||||
type UserPermissionsModules struct {
|
||||
AdditionalProperties map[string]map[string]bool `json:"-"`
|
||||
}
|
||||
|
||||
func (m *UserPermissionsModules) UnmarshalJSON(data []byte) error {
|
||||
type Alias UserPermissionsModules
|
||||
var known Alias
|
||||
if err := json.Unmarshal(data, &known); err != nil {
|
||||
return err
|
||||
}
|
||||
*m = UserPermissionsModules(known)
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
m.AdditionalProperties = make(map[string]map[string]bool, len(raw))
|
||||
for key, value := range raw {
|
||||
var decoded map[string]bool
|
||||
if err := json.Unmarshal(value, &decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
m.AdditionalProperties[key] = decoded
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m UserPermissionsModules) MarshalJSON() ([]byte, error) {
|
||||
type Alias UserPermissionsModules
|
||||
encoded, err := json.Marshal(Alias(m))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var object map[string]json.RawMessage
|
||||
if err := json.Unmarshal(encoded, &object); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, value := range m.AdditionalProperties {
|
||||
encodedValue, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
object[key] = encodedValue
|
||||
}
|
||||
return json.Marshal(object)
|
||||
}
|
||||
|
||||
type UserPermissions struct {
|
||||
// IsRestricted Indicates whether this User's Peers view is restricted.
|
||||
IsRestricted bool `json:"is_restricted"`
|
||||
// Modules example value is defined in the OpenAPI schema.
|
||||
Modules map[string]map[string]bool `json:"modules"`
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
components:
|
||||
schemas:
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: User ID
|
||||
type: string
|
||||
example: google-oauth2|277474792786460067937
|
||||
email:
|
||||
description: User's email address
|
||||
type: string
|
||||
example: demo@netbird.io
|
||||
password:
|
||||
description: User's password. Only present when user is created (create user endpoint is called) and only when IdP supports user creation with password.
|
||||
type: string
|
||||
example: super_secure_password
|
||||
name:
|
||||
description: User's name from idp provider
|
||||
type: string
|
||||
example: Tom Schulz
|
||||
role:
|
||||
description: User's NetBird account role
|
||||
type: string
|
||||
example: admin
|
||||
status:
|
||||
description: User's status
|
||||
type: string
|
||||
enum: [ "active", "invited", "blocked" ]
|
||||
example: active
|
||||
last_login:
|
||||
description: Last time this user performed a login to the dashboard
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2023-05-05T09:00:35.477782Z"
|
||||
auto_groups:
|
||||
description: Group IDs to auto-assign to peers registered by this user
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7m0
|
||||
is_current:
|
||||
description: Is true if authenticated user is the same as this user
|
||||
type: boolean
|
||||
readOnly: true
|
||||
example: true
|
||||
is_service_user:
|
||||
description: Is true if this user is a service user
|
||||
type: boolean
|
||||
readOnly: true
|
||||
example: false
|
||||
is_blocked:
|
||||
description: Is true if this user is blocked. Blocked users can't use the system
|
||||
type: boolean
|
||||
example: false
|
||||
pending_approval:
|
||||
description: Is true if this user requires approval before being activated. Only applicable for users joining via domain matching when user_approval_required is enabled.
|
||||
type: boolean
|
||||
example: false
|
||||
issued:
|
||||
description: How user was issued by API or Integration
|
||||
type: string
|
||||
example: api
|
||||
idp_id:
|
||||
description: Identity provider ID (connector ID) that the user authenticated with. Only populated for users with Dex-encoded user IDs.
|
||||
type: string
|
||||
example: okta-abc123
|
||||
permissions:
|
||||
$ref: '#/components/schemas/UserPermissions'
|
||||
required:
|
||||
- id
|
||||
- email
|
||||
- name
|
||||
- role
|
||||
- auto_groups
|
||||
- status
|
||||
- is_blocked
|
||||
- pending_approval
|
||||
UserPermissions:
|
||||
type: object
|
||||
properties:
|
||||
is_restricted:
|
||||
type: boolean
|
||||
description: Indicates whether this User's Peers view is restricted
|
||||
modules:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
propertyNames:
|
||||
type: string
|
||||
description: The operation type
|
||||
propertyNames:
|
||||
type: string
|
||||
description: The module name
|
||||
example: { "networks": { "read": true, "create": false, "update": false, "delete": false }, "peers": { "read": false, "create": false, "update": false, "delete": false } }
|
||||
required:
|
||||
- modules
|
||||
- is_restricted
|
||||
UserRequest:
|
||||
type: object
|
||||
properties:
|
||||
role:
|
||||
description: User's NetBird account role
|
||||
type: string
|
||||
example: admin
|
||||
auto_groups:
|
||||
description: Group IDs to auto-assign to peers registered by this user
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7m0
|
||||
is_blocked:
|
||||
description: If set to true then user is blocked and can't use the system
|
||||
type: boolean
|
||||
example: false
|
||||
required:
|
||||
- role
|
||||
- auto_groups
|
||||
- is_blocked
|
||||
UserCreateRequest:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
description: User's Email to send invite to
|
||||
type: string
|
||||
example: demo@netbird.io
|
||||
name:
|
||||
description: User's full name
|
||||
type: string
|
||||
example: Tom Schulz
|
||||
role:
|
||||
description: User's NetBird account role
|
||||
type: string
|
||||
example: admin
|
||||
auto_groups:
|
||||
description: Group IDs to auto-assign to peers registered by this user
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7m0
|
||||
is_service_user:
|
||||
description: Is true if this user is a service user
|
||||
type: boolean
|
||||
example: false
|
||||
required:
|
||||
- role
|
||||
- auto_groups
|
||||
- is_service_user
|
||||
@@ -0,0 +1,129 @@
|
||||
UsersPath:
|
||||
get:
|
||||
summary: List all Users
|
||||
description: Returns a list of all users
|
||||
tags: [ Users ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: query
|
||||
name: service_user
|
||||
schema:
|
||||
type: boolean
|
||||
description: Filters users and returns either regular users or service users
|
||||
responses:
|
||||
'200':
|
||||
description: A JSON array of Users
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: './user_components.yaml#/components/schemas/User'
|
||||
'400':
|
||||
"$ref": "../openapi.yaml#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "../openapi.yaml#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "../openapi.yaml#/components/responses/forbidden"
|
||||
'422':
|
||||
"$ref": "../openapi.yaml#/components/responses/unprocessable"
|
||||
'500':
|
||||
"$ref": "../openapi.yaml#/components/responses/internal_error"
|
||||
post:
|
||||
summary: Create a User
|
||||
description: Creates a new service user or sends an invite to a regular user
|
||||
tags: [ Users ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
requestBody:
|
||||
description: User invite information
|
||||
required: true
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: './user_components.yaml#/components/schemas/UserCreateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A User object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './user_components.yaml#/components/schemas/User'
|
||||
'400':
|
||||
"$ref": "../openapi.yaml#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "../openapi.yaml#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "../openapi.yaml#/components/responses/forbidden"
|
||||
'422':
|
||||
"$ref": "../openapi.yaml#/components/responses/unprocessable"
|
||||
'500':
|
||||
"$ref": "../openapi.yaml#/components/responses/internal_error"
|
||||
UserPath:
|
||||
put:
|
||||
summary: Update a User
|
||||
description: Update information about a User
|
||||
tags: [ Users ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: userId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a user
|
||||
requestBody:
|
||||
description: User update
|
||||
required: true
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: './user_components.yaml#/components/schemas/UserRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A User object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './user_components.yaml#/components/schemas/User'
|
||||
'400':
|
||||
"$ref": "../openapi.yaml#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "../openapi.yaml#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "../openapi.yaml#/components/responses/forbidden"
|
||||
'422':
|
||||
"$ref": "../openapi.yaml#/components/responses/unprocessable"
|
||||
'500':
|
||||
"$ref": "../openapi.yaml#/components/responses/internal_error"
|
||||
delete:
|
||||
summary: Delete a User
|
||||
description: This method removes a user from accessing the system. For this leaves the IDP user intact unless the `--user-delete-from-idp` is passed to management startup.
|
||||
tags: [ Users ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: userId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a user
|
||||
responses:
|
||||
'200':
|
||||
description: Delete status code
|
||||
content: { }
|
||||
'400':
|
||||
"$ref": "../openapi.yaml#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "../openapi.yaml#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "../openapi.yaml#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "../openapi.yaml#/components/responses/internal_error"
|
||||
Reference in New Issue
Block a user