mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-21 21:09:07 +02:00
Merge remote-tracking branch 'origin/main' into embedded-vnc
# Conflicts: # client/cmd/up.go # client/internal/engine.go # client/internal/profilemanager/config.go # client/proto/daemon.pb.go # client/server/mdm.go # client/server/server.go # client/status/status.go # client/ui/main.go # client/ui/services/windowmanager.go # management/internals/shared/grpc/conversion.go # management/server/store/sql_store.go # management/server/store/sql_store_test.go # shared/management/networkmap/envelope.go # shared/management/proto/management.pb.go
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
package certposture
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/certposture/certtest"
|
||||
)
|
||||
|
||||
var (
|
||||
secret = []byte("test-secret")
|
||||
peerKey = []byte("peer-public-key-aaaaaaaaaaaaaaaa")
|
||||
otherKey = []byte("peer-public-key-bbbbbbbbbbbbbbbb")
|
||||
now = time.Now().Truncate(Window)
|
||||
)
|
||||
|
||||
func TestChallenger_NonceIsStableWithinWindowAndPerPeer(t *testing.T) {
|
||||
c := NewChallenger(secret)
|
||||
|
||||
assert.Equal(t, c.Nonce(peerKey, now), c.Nonce(peerKey, now.Add(time.Minute)))
|
||||
assert.NotEqual(t, c.Nonce(peerKey, now), c.Nonce(peerKey, now.Add(Window)))
|
||||
assert.NotEqual(t, c.Nonce(peerKey, now), c.Nonce(otherKey, now))
|
||||
assert.NotEqual(t, c.Nonce(peerKey, now), NewChallenger([]byte("other")).Nonce(peerKey, now))
|
||||
}
|
||||
|
||||
func TestChallenger_VerifyNonce(t *testing.T) {
|
||||
c := NewChallenger(secret)
|
||||
nonce := c.Nonce(peerKey, now)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
nonce []byte
|
||||
peerKey []byte
|
||||
at time.Time
|
||||
wantErr error
|
||||
}{
|
||||
{"current window", nonce, peerKey, now, nil},
|
||||
{"previous window still accepted", nonce, peerKey, now.Add(Window), nil},
|
||||
{"two windows later expired", nonce, peerKey, now.Add(2 * Window), ErrNonceExpired},
|
||||
// An instance whose clock runs a little ahead issues the next window's nonce early.
|
||||
{"issued one window ahead accepted", c.Nonce(peerKey, now.Add(Window)), peerKey, now, nil},
|
||||
{"issued two windows ahead rejected", c.Nonce(peerKey, now.Add(2*Window)), peerKey, now, ErrNonceExpired},
|
||||
{"other peer", nonce, otherKey, now, ErrNonceMismatch},
|
||||
{"tampered mac", tamper(nonce, len(nonce)-1), peerKey, now, ErrNonceMismatch},
|
||||
{"malformed", nonce[:10], peerKey, now, ErrNonceMalformed},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := c.verifyNonce(tt.nonce, tt.peerKey, tt.at)
|
||||
assert.ErrorIs(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNonceAcceptedAlongside must agree with verifyNonce: a proof for a nonce is still
|
||||
// accepted while the next window's nonce is issued, and no longer two windows on.
|
||||
func TestNonceAcceptedAlongside(t *testing.T) {
|
||||
c := NewChallenger(secret)
|
||||
issued := c.Nonce(peerKey, now)
|
||||
|
||||
assert.True(t, NonceAcceptedAlongside(issued, issued), "the current nonce is accepted")
|
||||
assert.True(t, NonceAcceptedAlongside(issued, c.Nonce(peerKey, now.Add(Window))), "a proof from the previous window is accepted")
|
||||
assert.False(t, NonceAcceptedAlongside(issued, c.Nonce(peerKey, now.Add(2*Window))), "a proof two windows old is not")
|
||||
assert.True(t, NonceAcceptedAlongside(c.Nonce(peerKey, now.Add(Window)), issued), "a nonce from an instance one window ahead is accepted")
|
||||
assert.False(t, NonceAcceptedAlongside(c.Nonce(peerKey, now.Add(2*Window)), issued), "a nonce two windows ahead is not")
|
||||
assert.False(t, NonceAcceptedAlongside([]byte("short"), issued), "a malformed nonce is not")
|
||||
|
||||
for _, tt := range []struct {
|
||||
later time.Duration
|
||||
want bool
|
||||
}{{0, true}, {Window, true}, {2 * Window, false}} {
|
||||
err := c.verifyNonce(issued, peerKey, now.Add(tt.later))
|
||||
assert.Equal(t, tt.want, err == nil, "verifyNonce agrees %s later", tt.later)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignVerify_RoundTripPerKeyType(t *testing.T) {
|
||||
ca := certtest.NewCA(t, "root")
|
||||
keys := map[string]crypto.Signer{
|
||||
SigAlgECDSASHA256: certtest.ECDSAKey(t),
|
||||
SigAlgECDSASHA384: p384Key(t),
|
||||
SigAlgRSAPSSSHA256: certtest.RSAKey(t),
|
||||
SigAlgEd25519: certtest.Ed25519Key(t),
|
||||
}
|
||||
for wantAlg, key := range keys {
|
||||
t.Run(wantAlg, func(t *testing.T) {
|
||||
c := NewChallenger(secret)
|
||||
proof := signedProof(t, c, ca, key)
|
||||
assert.Equal(t, wantAlg, proof.SigAlg)
|
||||
|
||||
chain, err := c.Verify(proof, peerKey, now)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, chain, 1)
|
||||
assert.NoError(t, VerifyChain(chain, mustPool(t, ca.PEM), now))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func p384Key(t *testing.T) crypto.Signer {
|
||||
t.Helper()
|
||||
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
return key
|
||||
}
|
||||
|
||||
// TestSign_RefusesCurvesTheProofDoesNotName: a proof names its algorithm from the key, and
|
||||
// the verifier only knows P-256 and P-384, so P-521 is refused before anything is signed.
|
||||
func TestSign_RefusesCurvesTheProofDoesNotName(t *testing.T) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = Sign(key, NewChallenger(secret).Nonce(peerKey, now), peerKey)
|
||||
|
||||
assert.ErrorIs(t, err, ErrUnsupportedKey)
|
||||
}
|
||||
|
||||
func TestVerify_Rejections(t *testing.T) {
|
||||
ca := certtest.NewCA(t, "root")
|
||||
c := NewChallenger(secret)
|
||||
good := signedProof(t, c, ca, certtest.ECDSAKey(t))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(p Proof) Proof
|
||||
peerKey []byte
|
||||
wantErr error
|
||||
}{
|
||||
{"replayed for other peer", identity, otherKey, ErrNonceMismatch},
|
||||
{"signature tampered", func(p Proof) Proof { p.Signature = tamper(p.Signature, 5); return p }, peerKey, ErrSignatureInvalid},
|
||||
{"nonce swapped after signing", func(p Proof) Proof { p.Nonce = c.Nonce(peerKey, now.Add(-Window)); return p }, peerKey, ErrSignatureInvalid},
|
||||
{"foreign leaf presented", func(p Proof) Proof {
|
||||
p.Chain = [][]byte{ca.Issue(t, certtest.ECDSAKey(t), "other").Raw}
|
||||
return p
|
||||
}, peerKey, ErrSignatureInvalid},
|
||||
{"alg mismatch", func(p Proof) Proof { p.SigAlg = SigAlgEd25519; return p }, peerKey, ErrSigAlgMismatch},
|
||||
{"empty chain", func(p Proof) Proof { p.Chain = nil; return p }, peerKey, ErrEmptyChain},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := c.Verify(tt.mutate(good), tt.peerKey, now)
|
||||
assert.ErrorIs(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerify_SecretMismatchAcrossChallengers(t *testing.T) {
|
||||
ca := certtest.NewCA(t, "root")
|
||||
proof := signedProof(t, NewChallenger(secret), ca, certtest.ECDSAKey(t))
|
||||
|
||||
_, err := NewChallenger([]byte("other-instance-secret")).Verify(proof, peerKey, now)
|
||||
assert.ErrorIs(t, err, ErrNonceMismatch)
|
||||
}
|
||||
|
||||
func TestChainMatchesCAs(t *testing.T) {
|
||||
ca := certtest.NewCA(t, "root")
|
||||
otherCA := certtest.NewCA(t, "other-root")
|
||||
leaf := ca.Issue(t, certtest.ECDSAKey(t), "device")
|
||||
chainPEM := EncodeChainPEM([]*x509.Certificate{leaf})
|
||||
|
||||
assert.True(t, ChainMatchesCAs(chainPEM, []string{ca.PEM}, now))
|
||||
assert.True(t, ChainMatchesCAs(chainPEM, []string{otherCA.PEM, ca.PEM}, now))
|
||||
assert.False(t, ChainMatchesCAs(chainPEM, []string{otherCA.PEM}, now))
|
||||
assert.False(t, ChainMatchesCAs(chainPEM, []string{ca.PEM}, now.Add(30*24*time.Hour)))
|
||||
assert.False(t, ChainMatchesCAs(chainPEM, []string{"not a pem"}, now))
|
||||
assert.False(t, ChainMatchesCAs("not a pem", []string{ca.PEM}, now))
|
||||
}
|
||||
|
||||
func TestChainPEM_RoundTrip(t *testing.T) {
|
||||
ca := certtest.NewCA(t, "root")
|
||||
leaf := ca.Issue(t, certtest.ECDSAKey(t), "device")
|
||||
|
||||
chain, err := ParseChainPEM(EncodeChainPEM([]*x509.Certificate{leaf, ca.Cert}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, chain, 2)
|
||||
assert.Equal(t, leaf.Raw, chain[0].Raw)
|
||||
assert.Equal(t, ca.Cert.Raw, chain[1].Raw)
|
||||
}
|
||||
|
||||
func TestVerify_AcceptsMaximumPSSSalt(t *testing.T) {
|
||||
// A TPM chooses the PSS salt itself and older firmware uses the largest salt that
|
||||
// fits, so a proof from such a key carries more salt than Sign asks a software key for.
|
||||
ca := certtest.NewCA(t, "root")
|
||||
key := certtest.RSAKey(t).(*rsa.PrivateKey)
|
||||
leaf := ca.Issue(t, key, "device")
|
||||
c := NewChallenger(secret)
|
||||
nonce := c.Nonce(peerKey, now)
|
||||
digest := sha256.Sum256(proofMessage(nonce, peerKey))
|
||||
sig, err := rsa.SignPSS(rand.Reader, key, crypto.SHA256, digest[:], &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthAuto})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Verify(Proof{Nonce: nonce, Chain: [][]byte{leaf.Raw}, SigAlg: SigAlgRSAPSSSHA256, Signature: sig}, peerKey, now)
|
||||
assert.NoError(t, err, "a valid PSS signature must verify regardless of salt length")
|
||||
}
|
||||
|
||||
func signedProof(t *testing.T, c *Challenger, ca *certtest.CA, key crypto.Signer) Proof {
|
||||
t.Helper()
|
||||
leaf := ca.Issue(t, key, "device")
|
||||
nonce := c.Nonce(peerKey, now)
|
||||
sigAlg, sig, err := Sign(key, nonce, peerKey)
|
||||
require.NoError(t, err)
|
||||
return Proof{Nonce: nonce, Chain: [][]byte{leaf.Raw}, SigAlg: sigAlg, Signature: sig}
|
||||
}
|
||||
|
||||
func mustPool(t *testing.T, pems ...string) *x509.CertPool {
|
||||
t.Helper()
|
||||
pool, err := ParseCAs(pems)
|
||||
require.NoError(t, err)
|
||||
return pool
|
||||
}
|
||||
|
||||
func identity(p Proof) Proof { return p }
|
||||
|
||||
func tamper(b []byte, i int) []byte {
|
||||
out := append([]byte(nil), b...)
|
||||
out[i] ^= 0xff
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Package certtest builds throwaway CAs and leaf certificates for certificate posture tests.
|
||||
package certtest
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type CA struct {
|
||||
Cert *x509.Certificate
|
||||
Key crypto.Signer
|
||||
PEM string
|
||||
}
|
||||
|
||||
func NewCA(t *testing.T, name string) *CA {
|
||||
t.Helper()
|
||||
return newCA(t, name, nil)
|
||||
}
|
||||
|
||||
// NewIntermediate creates a CA signed by parent.
|
||||
func NewIntermediate(t *testing.T, parent *CA, name string) *CA {
|
||||
t.Helper()
|
||||
return newCA(t, name, parent)
|
||||
}
|
||||
|
||||
func newCA(t *testing.T, name string, parent *CA) *CA {
|
||||
t.Helper()
|
||||
key := ECDSAKey(t)
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
Subject: pkix.Name{CommonName: name},
|
||||
NotBefore: time.Now().Add(-24 * time.Hour),
|
||||
NotAfter: time.Now().Add(48 * time.Hour),
|
||||
IsCA: true,
|
||||
BasicConstraintsValid: true,
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
|
||||
}
|
||||
issuer, issuerKey := tmpl, key
|
||||
if parent != nil {
|
||||
issuer, issuerKey = parent.Cert, parent.Key
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, issuer, key.Public(), issuerKey)
|
||||
require.NoError(t, err)
|
||||
cert, err := x509.ParseCertificate(der)
|
||||
require.NoError(t, err)
|
||||
return &CA{Cert: cert, Key: key, PEM: CertPEM(cert)}
|
||||
}
|
||||
|
||||
// Issue signs a leaf certificate for key with the CA.
|
||||
func (ca *CA) Issue(t *testing.T, key crypto.Signer, cn string) *x509.Certificate {
|
||||
t.Helper()
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
Subject: pkix.Name{CommonName: cn},
|
||||
NotBefore: time.Now().Add(-24 * time.Hour),
|
||||
NotAfter: time.Now().Add(48 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, ca.Cert, key.Public(), ca.Key)
|
||||
require.NoError(t, err)
|
||||
cert, err := x509.ParseCertificate(der)
|
||||
require.NoError(t, err)
|
||||
return cert
|
||||
}
|
||||
|
||||
func ECDSAKey(t *testing.T) crypto.Signer {
|
||||
t.Helper()
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
return key
|
||||
}
|
||||
|
||||
func RSAKey(t *testing.T) crypto.Signer {
|
||||
t.Helper()
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
return key
|
||||
}
|
||||
|
||||
func Ed25519Key(t *testing.T) crypto.Signer {
|
||||
t.Helper()
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
return key
|
||||
}
|
||||
|
||||
func CertPEM(cert *x509.Certificate) string {
|
||||
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}))
|
||||
}
|
||||
|
||||
func KeyPEM(t *testing.T, key crypto.Signer) string {
|
||||
t.Helper()
|
||||
der, err := x509.MarshalPKCS8PrivateKey(key)
|
||||
require.NoError(t, err)
|
||||
return string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}))
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package certposture
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrNoCertificateInPEM = errors.New("no certificate found in PEM data")
|
||||
|
||||
// ParseCAs builds a root pool from PEM encoded CA certificates.
|
||||
func ParseCAs(pems []string) (*x509.CertPool, error) {
|
||||
roots := x509.NewCertPool()
|
||||
for _, p := range pems {
|
||||
if !roots.AppendCertsFromPEM([]byte(p)) {
|
||||
return nil, ErrNoCertificateInPEM
|
||||
}
|
||||
}
|
||||
return roots, nil
|
||||
}
|
||||
|
||||
// VerifyChain reports whether the leaf (chain[0]) chains to one of roots using the
|
||||
// remaining certificates as intermediates.
|
||||
func VerifyChain(chain []*x509.Certificate, roots *x509.CertPool, now time.Time) error {
|
||||
if len(chain) == 0 {
|
||||
return ErrEmptyChain
|
||||
}
|
||||
_, err := VerifiedChain(chain[0], chain[1:], roots, now)
|
||||
return err
|
||||
}
|
||||
|
||||
// VerifiedChain finds a path from leaf to one of roots through any of intermediates,
|
||||
// trying every candidate issuer rather than the first that matches, and returns it
|
||||
// without the root, leaf first. It applies the same rules as VerifyChain.
|
||||
func VerifiedChain(leaf *x509.Certificate, intermediates []*x509.Certificate, roots *x509.CertPool, now time.Time) ([]*x509.Certificate, error) {
|
||||
pool := x509.NewCertPool()
|
||||
for _, cert := range intermediates {
|
||||
pool.AddCert(cert)
|
||||
}
|
||||
chains, err := leaf.Verify(x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
Intermediates: pool,
|
||||
CurrentTime: now,
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chain := chains[0]
|
||||
if len(chain) > 1 {
|
||||
chain = chain[:len(chain)-1]
|
||||
}
|
||||
return chain, nil
|
||||
}
|
||||
|
||||
// ChainMatchesCAs is VerifyChain over the PEM forms stored in peer meta and check config.
|
||||
func ChainMatchesCAs(chainPEM string, caPEMs []string, now time.Time) bool {
|
||||
roots, err := ParseCAs(caPEMs)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return chainMatchesPool(chainPEM, roots, now)
|
||||
}
|
||||
|
||||
// AnyChainMatchesCAs reports whether at least one of the peer's verified chains
|
||||
// is anchored in one of the configured CAs.
|
||||
func AnyChainMatchesCAs(chainPEMs, caPEMs []string, now time.Time) bool {
|
||||
roots, err := ParseCAs(caPEMs)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, chainPEM := range chainPEMs {
|
||||
if chainMatchesPool(chainPEM, roots, now) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func chainMatchesPool(chainPEM string, roots *x509.CertPool, now time.Time) bool {
|
||||
chain, err := ParseChainPEM(chainPEM)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return VerifyChain(chain, roots, now) == nil
|
||||
}
|
||||
|
||||
func EncodeChainPEM(chain []*x509.Certificate) string {
|
||||
var b strings.Builder
|
||||
for _, cert := range chain {
|
||||
_ = pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func ParseChainPEM(chainPEM string) ([]*x509.Certificate, error) {
|
||||
var chain []*x509.Certificate
|
||||
rest := []byte(chainPEM)
|
||||
for {
|
||||
var block *pem.Block
|
||||
block, rest = pem.Decode(rest)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
if block.Type != "CERTIFICATE" {
|
||||
continue
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse certificate: %w", err)
|
||||
}
|
||||
chain = append(chain, cert)
|
||||
}
|
||||
if len(chain) == 0 {
|
||||
return nil, ErrNoCertificateInPEM
|
||||
}
|
||||
return chain, nil
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package certposture
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// Window is the default challenge window. A nonce stays valid through the window
|
||||
// after the one it was issued in, so a peer re-proves possession of its key between
|
||||
// once and twice per window. One window early is accepted too, for clock skew.
|
||||
Window = 12 * time.Hour
|
||||
|
||||
// EnvWindow overrides Window, for end-to-end tests that cannot wait half a day to
|
||||
// watch a renewal. Every management instance has to be given the same value: the
|
||||
// window is part of the nonce, so instances that disagree reject each other's.
|
||||
EnvWindow = "NB_CERT_CHALLENGE_WINDOW"
|
||||
|
||||
// minWindow keeps the renewal period, a third of the window, well above the
|
||||
// refresher's one-second tick.
|
||||
minWindow = 30 * time.Second
|
||||
maxWindow = 24 * time.Hour
|
||||
|
||||
challengeDomain = "netbird-cert-challenge-v1"
|
||||
windowLen = 8
|
||||
nonceLen = windowLen + sha256.Size
|
||||
|
||||
// NonceSize is the length of every nonce a Challenger issues.
|
||||
NonceSize = nonceLen
|
||||
)
|
||||
|
||||
var effectiveWindow = sync.OnceValue(resolveWindow)
|
||||
|
||||
// EffectiveWindow returns the challenge window in force, which is Window unless
|
||||
// EnvWindow overrides it. Everything timed against the window derives from this, so a
|
||||
// test that shortens it shortens the renewal that goes with it.
|
||||
func EffectiveWindow() time.Duration {
|
||||
return effectiveWindow()
|
||||
}
|
||||
|
||||
func resolveWindow() time.Duration {
|
||||
val := os.Getenv(EnvWindow)
|
||||
if val == "" {
|
||||
return Window
|
||||
}
|
||||
|
||||
window, err := time.ParseDuration(val)
|
||||
if err != nil {
|
||||
log.Warnf("failed to parse %s, keeping the %s certificate challenge window: %v", EnvWindow, Window, err)
|
||||
return Window
|
||||
}
|
||||
if window < minWindow || window > maxWindow {
|
||||
log.Warnf("%s of %s is outside %s..%s, keeping the %s certificate challenge window", EnvWindow, window, minWindow, maxWindow, Window)
|
||||
return Window
|
||||
}
|
||||
if window%time.Second != 0 {
|
||||
log.Warnf("%s of %s is not a whole number of seconds, keeping the %s certificate challenge window", EnvWindow, window, Window)
|
||||
return Window
|
||||
}
|
||||
|
||||
// Loud on purpose: this sets how long a device can pass the certificate check after
|
||||
// its key has gone, and it has to match on every instance.
|
||||
log.Warnf("certificate challenge window overridden to %s by %s", window, EnvWindow)
|
||||
return window
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNonceMalformed = errors.New("certificate challenge nonce is malformed")
|
||||
ErrNonceExpired = errors.New("certificate challenge nonce is expired")
|
||||
ErrNonceMismatch = errors.New("certificate challenge nonce was not issued to this peer")
|
||||
)
|
||||
|
||||
// Challenger issues and verifies stateless per-peer nonces. A nonce is bound to the
|
||||
// peer and to a time window, so any instance sharing the secret can verify it.
|
||||
type Challenger struct {
|
||||
secret []byte
|
||||
window time.Duration
|
||||
}
|
||||
|
||||
func NewChallenger(secret []byte) *Challenger {
|
||||
return &Challenger{secret: secret, window: EffectiveWindow()}
|
||||
}
|
||||
|
||||
func (c *Challenger) Nonce(peerKey []byte, now time.Time) []byte {
|
||||
return c.nonceForWindow(peerKey, c.windowOf(now))
|
||||
}
|
||||
|
||||
func (c *Challenger) verifyNonce(nonce, peerKey []byte, now time.Time) error {
|
||||
if len(nonce) != nonceLen {
|
||||
return ErrNonceMalformed
|
||||
}
|
||||
window := binary.BigEndian.Uint64(nonce[:windowLen])
|
||||
if !windowAccepted(window, c.windowOf(now)) {
|
||||
return ErrNonceExpired
|
||||
}
|
||||
if !hmac.Equal(nonce, c.nonceForWindow(peerKey, window)) {
|
||||
return ErrNonceMismatch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Challenger) windowOf(now time.Time) uint64 {
|
||||
return uint64(now.Unix() / int64(c.window.Seconds()))
|
||||
}
|
||||
|
||||
func (c *Challenger) nonceForWindow(peerKey []byte, window uint64) []byte {
|
||||
nonce := make([]byte, windowLen, nonceLen)
|
||||
binary.BigEndian.PutUint64(nonce, window)
|
||||
|
||||
mac := hmac.New(sha256.New, c.secret)
|
||||
mac.Write([]byte(challengeDomain))
|
||||
mac.Write(peerKey)
|
||||
mac.Write(nonce[:windowLen])
|
||||
return mac.Sum(nonce)
|
||||
}
|
||||
|
||||
// NonceAcceptedAlongside reports whether a proof answering nonce is still accepted while
|
||||
// management issues current to the same peer, by the same window rule verification uses.
|
||||
func NonceAcceptedAlongside(nonce, current []byte) bool {
|
||||
if len(nonce) != nonceLen || len(current) != nonceLen {
|
||||
return false
|
||||
}
|
||||
window := binary.BigEndian.Uint64(nonce[:windowLen])
|
||||
return windowAccepted(window, binary.BigEndian.Uint64(current[:windowLen]))
|
||||
}
|
||||
|
||||
// windowAccepted reports whether a nonce of window is accepted in window current: the
|
||||
// current window, the previous one so a nonce outlives a rollover, and the next one so an
|
||||
// instance whose clock runs slightly ahead is not rejected by the others.
|
||||
func windowAccepted(window, current uint64) bool {
|
||||
return window == current || window+1 == current || window == current+1
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package certposture
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResolveWindow(t *testing.T) {
|
||||
// An end-to-end run shortens the window so a renewal can be watched in seconds
|
||||
// rather than half a day. Anything it cannot make sense of leaves the default in
|
||||
// place, because a window nobody intended is a security property nobody chose.
|
||||
tests := []struct {
|
||||
name string
|
||||
env string
|
||||
want time.Duration
|
||||
}{
|
||||
{name: "unset keeps the default", env: "", want: Window},
|
||||
{name: "a test-sized window is taken", env: "30s", want: 30 * time.Second},
|
||||
{name: "garbage keeps the default", env: "soon", want: Window},
|
||||
{name: "below the floor keeps the default", env: "10ms", want: Window},
|
||||
{name: "too short for the refresh tick keeps the default", env: "1s", want: Window},
|
||||
{name: "a fractional second keeps the default", env: "30500ms", want: Window},
|
||||
{name: "above the ceiling keeps the default", env: "100h", want: Window},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv(EnvWindow, tt.env)
|
||||
if tt.env == "" {
|
||||
require.NoError(t, os.Unsetenv(EnvWindow))
|
||||
}
|
||||
assert.Equal(t, tt.want, resolveWindow())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChallenger_HonoursAShortenedWindow(t *testing.T) {
|
||||
// The window is what a nonce is stamped with, so a shortened one has to make a
|
||||
// nonce expire sooner, not just change a number in a log line.
|
||||
short := 2 * time.Second
|
||||
c := &Challenger{secret: []byte("secret"), window: short}
|
||||
peerKey := []byte("peer-key")
|
||||
|
||||
issued := time.Unix(1_790_000_000, 0)
|
||||
nonce := c.Nonce(peerKey, issued)
|
||||
|
||||
require.NoError(t, c.verifyNonce(nonce, peerKey, issued), "a nonce is valid when issued")
|
||||
require.NoError(t, c.verifyNonce(nonce, peerKey, issued.Add(short)), "and through the window after it")
|
||||
assert.ErrorIs(t, c.verifyNonce(nonce, peerKey, issued.Add(3*short)), ErrNonceExpired,
|
||||
"a shortened window must actually expire the nonce sooner")
|
||||
}
|
||||
|
||||
// heldNonceAlwaysAccepted walks ten windows and, at every step, checks the nonce the peer
|
||||
// would be holding if it were re-stamped every period starting at offset. It returns how
|
||||
// many of those checks would have rejected the peer.
|
||||
func heldNonceAlwaysAccepted(c *Challenger, peerKey []byte, period, offset time.Duration) int {
|
||||
start := time.Unix(0, 0).UTC().Add(offset)
|
||||
var rejected int
|
||||
for elapsed := time.Duration(0); elapsed < 10*Window; elapsed += 7 * time.Minute {
|
||||
periods := int64(elapsed / period)
|
||||
lastRefresh := start.Add(time.Duration(periods) * period)
|
||||
if c.verifyNonce(c.Nonce(peerKey, lastRefresh), peerKey, start.Add(elapsed)) != nil {
|
||||
rejected++
|
||||
}
|
||||
}
|
||||
return rejected
|
||||
}
|
||||
|
||||
// TestNonce_StaysValidWhenRestampedWithinAWindow is the reason management keeps no per-peer
|
||||
// nonce state. A nonce carries the window it was minted in, not the instant, and is accepted
|
||||
// for that window and the next. A peer re-stamped at least once per window therefore can
|
||||
// never be holding one that has fallen outside the accepted pair, whoever it is and whenever
|
||||
// it was last served. There is nothing to track and nothing to search for: the guarantee
|
||||
// comes from the cadence alone.
|
||||
//
|
||||
// The offsets matter: each account is given a phase of its own so a restart does not fan out
|
||||
// to every account at once, so the property has to hold off the window boundary too.
|
||||
func TestNonce_StaysValidWhenRestampedWithinAWindow(t *testing.T) {
|
||||
c := &Challenger{secret: []byte("secret"), window: Window}
|
||||
peerKey := []byte("peer-key")
|
||||
|
||||
for _, period := range []time.Duration{Window / 3, Window / 2, Window} {
|
||||
for _, offset := range []time.Duration{0, Window / 7, Window / 2, Window - time.Minute} {
|
||||
t.Run(period.String()+"+"+offset.String(), func(t *testing.T) {
|
||||
assert.Zero(t, heldNonceAlwaysAccepted(c, peerKey, period, offset),
|
||||
"a peer re-stamped every %v is never left holding an expired nonce", period)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonce_ExpiresWhenRestampedTooSlowly(t *testing.T) {
|
||||
// The counterpart, which is what gives the test above its teeth. Note the aligned case
|
||||
// does not fail: a cadence of exactly two windows that lands on the boundaries is
|
||||
// covered by the grace window. Off the boundary it is not, and real refreshes are off
|
||||
// the boundary by design.
|
||||
c := &Challenger{secret: []byte("secret"), window: Window}
|
||||
peerKey := []byte("peer-key")
|
||||
|
||||
assert.Zero(t, heldNonceAlwaysAccepted(c, peerKey, 2*Window, 0),
|
||||
"two windows exactly on the boundary happens to be covered by the grace window")
|
||||
assert.NotZero(t, heldNonceAlwaysAccepted(c, peerKey, 2*Window, Window/2),
|
||||
"the same cadence off the boundary must leave the peer rejected for a stretch")
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package certposture
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
SigAlgECDSASHA256 = "ecdsa-sha256"
|
||||
SigAlgECDSASHA384 = "ecdsa-sha384"
|
||||
SigAlgRSAPSSSHA256 = "rsa-pss-sha256"
|
||||
SigAlgEd25519 = "ed25519"
|
||||
|
||||
proofDomain = "netbird-posture-cert-v1"
|
||||
minRSABits = 2048
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnsupportedKey = errors.New("unsupported certificate key")
|
||||
ErrEmptyChain = errors.New("certificate chain is empty")
|
||||
ErrSignatureInvalid = errors.New("certificate proof signature is invalid")
|
||||
ErrSigAlgMismatch = errors.New("signature algorithm does not match the certificate key")
|
||||
)
|
||||
|
||||
// Proof is a client's demonstration that it holds the private key of the leaf
|
||||
// certificate in Chain, made by signing the challenge nonce bound to its peer key.
|
||||
type Proof struct {
|
||||
Nonce []byte
|
||||
Chain [][]byte
|
||||
SigAlg string
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
// Sign produces the proof signature for a nonce using the leaf's private key. The key
|
||||
// never leaves the signer, which may be backed by a file, a TPM or an OS keystore.
|
||||
func Sign(signer crypto.Signer, nonce, peerKey []byte) (string, []byte, error) {
|
||||
sigAlg, err := sigAlgFor(signer.Public())
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
msg := proofMessage(nonce, peerKey)
|
||||
var sig []byte
|
||||
switch sigAlg {
|
||||
case SigAlgECDSASHA256:
|
||||
d := sha256.Sum256(msg)
|
||||
sig, err = signer.Sign(rand.Reader, d[:], crypto.SHA256)
|
||||
case SigAlgECDSASHA384:
|
||||
d := sha512.Sum384(msg)
|
||||
sig, err = signer.Sign(rand.Reader, d[:], crypto.SHA384)
|
||||
case SigAlgRSAPSSSHA256:
|
||||
d := sha256.Sum256(msg)
|
||||
sig, err = signer.Sign(rand.Reader, d[:], &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash, Hash: crypto.SHA256})
|
||||
case SigAlgEd25519:
|
||||
sig, err = signer.Sign(rand.Reader, msg, crypto.Hash(0))
|
||||
}
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("sign certificate proof: %w", err)
|
||||
}
|
||||
return sigAlg, sig, nil
|
||||
}
|
||||
|
||||
// Verify checks that the proof's nonce was issued by this challenger to peerKey and is
|
||||
// still fresh, and that the signature validates against the leaf's public key. It
|
||||
// returns the parsed chain on success. Chain trust is deliberately not evaluated here.
|
||||
func (c *Challenger) Verify(proof Proof, peerKey []byte, now time.Time) ([]*x509.Certificate, error) {
|
||||
if err := c.verifyNonce(proof.Nonce, peerKey, now); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chain, err := parseChain(proof.Chain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
leaf := chain[0]
|
||||
sigAlg, err := sigAlgFor(leaf.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sigAlg != proof.SigAlg {
|
||||
return nil, ErrSigAlgMismatch
|
||||
}
|
||||
if !verifySignature(leaf.PublicKey, sigAlg, proofMessage(proof.Nonce, peerKey), proof.Signature) {
|
||||
return nil, ErrSignatureInvalid
|
||||
}
|
||||
return chain, nil
|
||||
}
|
||||
|
||||
func proofMessage(nonce, peerKey []byte) []byte {
|
||||
msg := make([]byte, 0, len(proofDomain)+len(nonce)+len(peerKey))
|
||||
msg = append(msg, proofDomain...)
|
||||
msg = append(msg, nonce...)
|
||||
return append(msg, peerKey...)
|
||||
}
|
||||
|
||||
func sigAlgFor(pub crypto.PublicKey) (string, error) {
|
||||
switch k := pub.(type) {
|
||||
case *ecdsa.PublicKey:
|
||||
switch k.Curve {
|
||||
case elliptic.P256():
|
||||
return SigAlgECDSASHA256, nil
|
||||
case elliptic.P384():
|
||||
return SigAlgECDSASHA384, nil
|
||||
}
|
||||
case *rsa.PublicKey:
|
||||
if k.N.BitLen() >= minRSABits {
|
||||
return SigAlgRSAPSSSHA256, nil
|
||||
}
|
||||
case ed25519.PublicKey:
|
||||
return SigAlgEd25519, nil
|
||||
}
|
||||
return "", ErrUnsupportedKey
|
||||
}
|
||||
|
||||
func verifySignature(pub crypto.PublicKey, sigAlg string, msg, sig []byte) bool {
|
||||
switch sigAlg {
|
||||
case SigAlgECDSASHA256:
|
||||
d := sha256.Sum256(msg)
|
||||
return ecdsa.VerifyASN1(pub.(*ecdsa.PublicKey), d[:], sig)
|
||||
case SigAlgECDSASHA384:
|
||||
d := sha512.Sum384(msg)
|
||||
return ecdsa.VerifyASN1(pub.(*ecdsa.PublicKey), d[:], sig)
|
||||
case SigAlgRSAPSSSHA256:
|
||||
d := sha256.Sum256(msg)
|
||||
return rsa.VerifyPSS(pub.(*rsa.PublicKey), crypto.SHA256, d[:], sig, &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthAuto}) == nil
|
||||
case SigAlgEd25519:
|
||||
return ed25519.Verify(pub.(ed25519.PublicKey), msg, sig)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseChain(der [][]byte) ([]*x509.Certificate, error) {
|
||||
if len(der) == 0 {
|
||||
return nil, ErrEmptyChain
|
||||
}
|
||||
chain := make([]*x509.Certificate, 0, len(der))
|
||||
for _, raw := range der {
|
||||
cert, err := x509.ParseCertificate(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse certificate: %w", err)
|
||||
}
|
||||
chain = append(chain, cert)
|
||||
}
|
||||
return chain, nil
|
||||
}
|
||||
@@ -21,7 +21,11 @@ type Client interface {
|
||||
// is not eligible for session extension.
|
||||
ExtendAuthSession(sysInfo *system.Info, jwtToken string) (*proto.ExtendAuthSessionResponse, error)
|
||||
GetDeviceAuthorizationFlow() (*proto.DeviceAuthorizationFlow, error)
|
||||
GetPKCEAuthorizationFlow() (*proto.PKCEAuthorizationFlow, error)
|
||||
// GetPKCEAuthorizationFlow returns the PKCE authorization flow information.
|
||||
//
|
||||
// sessionExtend marks the flow as renewing an existing peer's session rather
|
||||
// than logging one in. See PKCEAuthorizationFlowRequest.
|
||||
GetPKCEAuthorizationFlow(sessionExtend bool) (*proto.PKCEAuthorizationFlow, error)
|
||||
GetServerURL() string
|
||||
// IsHealthy returns the current connection status without blocking.
|
||||
// Used by the engine to monitor connectivity in the background.
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/mock/gomock"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
@@ -35,7 +35,6 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
||||
"github.com/netbirdio/netbird/management/server/groups"
|
||||
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
||||
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||
"github.com/netbirdio/netbird/management/server/permissions"
|
||||
"github.com/netbirdio/netbird/management/server/settings"
|
||||
@@ -128,8 +127,8 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
|
||||
|
||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
requestBuffer := mgmt.NewAccountRequestBuffer(ctx, store)
|
||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManger), config, nil)
|
||||
accountManager, err := mgmt.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false, cacheStore)
|
||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", ephemeral_manager.NewEphemeralManager(store, peersManger), config, nil)
|
||||
accountManager, err := mgmt.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, ia, metrics, settingsMockManager, permissionsManagerMock, false, cacheStore)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -666,7 +665,12 @@ func Test_GetPKCEAuthorizationFlow(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
var gotRequest mgmtProto.PKCEAuthorizationFlowRequest
|
||||
mgmtMockServer.GetPKCEAuthorizationFlowFunc = func(ctx context.Context, req *mgmtProto.EncryptedMessage) (*mgmtProto.EncryptedMessage, error) {
|
||||
if err := encryption.DecryptMessage(client.key.PublicKey(), serverKey, req.Body, &gotRequest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encryptedResp, err := encryption.EncryptMessage(client.key.PublicKey(), serverKey, expectedFlowInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -679,10 +683,10 @@ func Test_GetPKCEAuthorizationFlow(t *testing.T) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
flowInfo, err := client.GetPKCEAuthorizationFlow()
|
||||
if err != nil {
|
||||
t.Error("error while retrieving pkce auth flow information")
|
||||
}
|
||||
flowInfo, err := client.GetPKCEAuthorizationFlow(true)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, gotRequest.GetSessionExtend(), "session extend should reach the server")
|
||||
|
||||
assert.Equal(t, expectedFlowInfo.ProviderConfig.ClientID, flowInfo.ProviderConfig.ClientID, "provider configured client ID should match")
|
||||
assert.Equal(t, expectedFlowInfo.ProviderConfig.ClientSecret, flowInfo.ProviderConfig.ClientSecret, "provider configured client secret should match") //nolint:staticcheck
|
||||
|
||||
@@ -621,7 +621,13 @@ func (c *GrpcClient) login(req *proto.LoginRequest) (*proto.LoginResponse, error
|
||||
|
||||
err = backoff.Retry(operation, nbgrpc.Backoff(c.ctx))
|
||||
if err != nil {
|
||||
log.Errorf("failed to login to Management Service: %v", err)
|
||||
// PermissionDenied is how management asks an unknown peer to register,
|
||||
// which the caller does next, so it is not an error worth reporting.
|
||||
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.PermissionDenied {
|
||||
log.Debugf("login to Management Service refused: %v", err)
|
||||
} else {
|
||||
log.Errorf("failed to login to Management Service: %v", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -738,9 +744,13 @@ func (c *GrpcClient) GetDeviceAuthorizationFlow() (*proto.DeviceAuthorizationFlo
|
||||
return flowInfoResp, nil
|
||||
}
|
||||
|
||||
// GetPKCEAuthorizationFlow returns a pkce authorization flow information.
|
||||
// GetPKCEAuthorizationFlow returns the PKCE authorization flow information.
|
||||
// It also takes care of encrypting and decrypting messages.
|
||||
func (c *GrpcClient) GetPKCEAuthorizationFlow() (*proto.PKCEAuthorizationFlow, error) {
|
||||
//
|
||||
// sessionExtend tells the server the flow will renew an existing peer's session
|
||||
// rather than log one in, so it can rule out a configuration that would let the
|
||||
// IdP answer from an unrelated account. See PKCEAuthorizationFlowRequest.
|
||||
func (c *GrpcClient) GetPKCEAuthorizationFlow(sessionExtend bool) (*proto.PKCEAuthorizationFlow, error) {
|
||||
if !c.ready() {
|
||||
return nil, fmt.Errorf("no connection to management in order to get pkce authorization flow")
|
||||
}
|
||||
@@ -753,7 +763,7 @@ func (c *GrpcClient) GetPKCEAuthorizationFlow() (*proto.PKCEAuthorizationFlow, e
|
||||
mgmCtx, cancel := context.WithTimeout(c.ctx, time.Second*2)
|
||||
defer cancel()
|
||||
|
||||
message := &proto.PKCEAuthorizationFlowRequest{}
|
||||
message := &proto.PKCEAuthorizationFlowRequest{SessionExtend: sessionExtend}
|
||||
encryptedMSG, err := encryption.EncryptMessage(*serverKey, c.key, message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1014,6 +1024,19 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta {
|
||||
})
|
||||
}
|
||||
|
||||
proofs := make([]*proto.CertificateProof, 0, len(info.CertificateProofs))
|
||||
for _, p := range info.CertificateProofs {
|
||||
proofs = append(proofs, &proto.CertificateProof{
|
||||
Nonce: p.Nonce,
|
||||
Chain: p.Chain,
|
||||
SigAlg: p.SigAlg,
|
||||
Signature: p.Signature,
|
||||
})
|
||||
}
|
||||
if len(proofs) > 0 {
|
||||
log.Debugf("peer meta carries %d certificate posture proofs", len(proofs))
|
||||
}
|
||||
|
||||
return &proto.PeerSystemMeta{
|
||||
Hostname: info.Hostname,
|
||||
GoOS: info.GoOS,
|
||||
@@ -1033,7 +1056,8 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta {
|
||||
Cloud: info.Environment.Cloud,
|
||||
Platform: info.Environment.Platform,
|
||||
},
|
||||
Files: files,
|
||||
Files: files,
|
||||
CertificateProofs: proofs,
|
||||
|
||||
Flags: &proto.Flags{
|
||||
RosenpassEnabled: info.RosenpassEnabled,
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
"github.com/netbirdio/netbird/client/system"
|
||||
"github.com/netbirdio/netbird/shared/management/certposture"
|
||||
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
@@ -93,3 +95,25 @@ func TestMaxRecvMsgSizeIntegration(t *testing.T) {
|
||||
assert.Len(t, resp.Key, payloadSize)
|
||||
})
|
||||
}
|
||||
|
||||
// TestInfoToMetaData_CarriesCertificateProofs: management verifies the proofs it receives
|
||||
// in the peer meta, so a field dropped by the mapping fails the certificate check without
|
||||
// any error on either side.
|
||||
func TestInfoToMetaData_CarriesCertificateProofs(t *testing.T) {
|
||||
info := &system.Info{CertificateProofs: []certposture.Proof{
|
||||
{Nonce: []byte("nonce-1"), Chain: [][]byte{[]byte("leaf"), []byte("intermediate")}, SigAlg: "ecdsa-sha256", Signature: []byte("sig-1")},
|
||||
{Nonce: []byte("nonce-2"), Chain: [][]byte{[]byte("other")}, SigAlg: "rsa-pss-sha256", Signature: []byte("sig-2")},
|
||||
}}
|
||||
|
||||
meta := infoToMetaData(info)
|
||||
|
||||
require.Len(t, meta.GetCertificateProofs(), 2, "every proof reaches the meta")
|
||||
first := meta.GetCertificateProofs()[0]
|
||||
assert.Equal(t, []byte("nonce-1"), first.GetNonce())
|
||||
assert.Equal(t, [][]byte{[]byte("leaf"), []byte("intermediate")}, first.GetChain(), "the whole chain is carried, in order")
|
||||
assert.Equal(t, "ecdsa-sha256", first.GetSigAlg())
|
||||
assert.Equal(t, []byte("sig-1"), first.GetSignature())
|
||||
assert.Equal(t, "rsa-pss-sha256", meta.GetCertificateProofs()[1].GetSigAlg())
|
||||
|
||||
assert.Empty(t, infoToMetaData(&system.Info{}).GetCertificateProofs(), "a device without proofs sends none")
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ type MockClient struct {
|
||||
LoginFunc func(info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error)
|
||||
ExtendAuthSessionFunc func(info *system.Info, jwtToken string) (*proto.ExtendAuthSessionResponse, error)
|
||||
GetDeviceAuthorizationFlowFunc func() (*proto.DeviceAuthorizationFlow, error)
|
||||
GetPKCEAuthorizationFlowFunc func() (*proto.PKCEAuthorizationFlow, error)
|
||||
GetPKCEAuthorizationFlowFunc func(sessionExtend bool) (*proto.PKCEAuthorizationFlow, error)
|
||||
GetServerURLFunc func() string
|
||||
HealthCheckFunc func() error
|
||||
SyncMetaFunc func(sysInfo *system.Info) error
|
||||
@@ -80,11 +80,12 @@ func (m *MockClient) GetDeviceAuthorizationFlow() (*proto.DeviceAuthorizationFlo
|
||||
return m.GetDeviceAuthorizationFlowFunc()
|
||||
}
|
||||
|
||||
func (m *MockClient) GetPKCEAuthorizationFlow() (*proto.PKCEAuthorizationFlow, error) {
|
||||
// GetPKCEAuthorizationFlow returns the PKCE authorization flow information.
|
||||
func (m *MockClient) GetPKCEAuthorizationFlow(sessionExtend bool) (*proto.PKCEAuthorizationFlow, error) {
|
||||
if m.GetPKCEAuthorizationFlowFunc == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return m.GetPKCEAuthorizationFlowFunc()
|
||||
return m.GetPKCEAuthorizationFlowFunc(sessionExtend)
|
||||
}
|
||||
|
||||
func (m *MockClient) HealthCheck() error {
|
||||
|
||||
@@ -127,10 +127,6 @@ type Client struct {
|
||||
// see more: https://docs.netbird.io/api/resources/identity-providers
|
||||
IdentityProviders *IdentityProvidersAPI
|
||||
|
||||
// Ingress NetBird Ingress Peers APIs
|
||||
// see more: https://docs.netbird.io/api/resources/ingress-ports
|
||||
Ingress *IngressAPI
|
||||
|
||||
// Instance NetBird Instance API
|
||||
// see more: https://docs.netbird.io/api/resources/instance
|
||||
Instance *InstanceAPI
|
||||
@@ -207,7 +203,6 @@ func (c *Client) initialize() {
|
||||
c.OktaScimIDP = &OktaScimIDPAPI{c}
|
||||
c.EventStreaming = &EventStreamingAPI{c}
|
||||
c.IdentityProviders = &IdentityProvidersAPI{c}
|
||||
c.Ingress = &IngressAPI{c}
|
||||
c.Instance = &InstanceAPI{c}
|
||||
c.ReverseProxyServices = &ReverseProxyServicesAPI{c}
|
||||
c.ReverseProxyClusters = &ReverseProxyClustersAPI{c}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
// IngressAPI APIs for Ingress Peers, do not use directly
|
||||
type IngressAPI struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// List all ingress peers
|
||||
// See more: https://docs.netbird.io/api/resources/ingress#list-all-ingress-peers
|
||||
func (a *IngressAPI) List(ctx context.Context) ([]api.IngressPeer, error) {
|
||||
resp, err := a.c.NewRequest(ctx, "GET", "/api/ingress/peers", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
ret, err := parseResponse[[]api.IngressPeer](resp)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// Get ingress peer info
|
||||
// See more: https://docs.netbird.io/api/resources/ingress#retrieve-an-ingress-peer
|
||||
func (a *IngressAPI) Get(ctx context.Context, ingressPeerID string) (*api.IngressPeer, error) {
|
||||
resp, err := a.c.NewRequest(ctx, "GET", "/api/ingress/peers/"+ingressPeerID, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
ret, err := parseResponse[api.IngressPeer](resp)
|
||||
return &ret, err
|
||||
}
|
||||
|
||||
// Create new ingress peer
|
||||
// See more: https://docs.netbird.io/api/resources/ingress#create-an-ingress-peer
|
||||
func (a *IngressAPI) Create(ctx context.Context, request api.PostApiIngressPeersJSONRequestBody) (*api.IngressPeer, error) {
|
||||
requestBytes, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := a.c.NewRequest(ctx, "POST", "/api/ingress/peers", bytes.NewReader(requestBytes), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
ret, err := parseResponse[api.IngressPeer](resp)
|
||||
return &ret, err
|
||||
}
|
||||
|
||||
// Update update ingress peer
|
||||
// See more: https://docs.netbird.io/api/resources/ingress#update-an-ingress-peer
|
||||
func (a *IngressAPI) Update(ctx context.Context, ingressPeerID string, request api.PutApiIngressPeersIngressPeerIdJSONRequestBody) (*api.IngressPeer, error) {
|
||||
requestBytes, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := a.c.NewRequest(ctx, "PUT", "/api/ingress/peers/"+ingressPeerID, bytes.NewReader(requestBytes), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
ret, err := parseResponse[api.IngressPeer](resp)
|
||||
return &ret, err
|
||||
}
|
||||
|
||||
// Delete delete ingress peer
|
||||
// See more: https://docs.netbird.io/api/resources/ingress#delete-an-ingress-peer
|
||||
func (a *IngressAPI) Delete(ctx context.Context, ingressPeerID string) error {
|
||||
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/ingress/peers/"+ingressPeerID, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
//go:build integration
|
||||
|
||||
package rest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/client/rest"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
"github.com/netbirdio/netbird/shared/management/http/util"
|
||||
)
|
||||
|
||||
var testIngressPeer = api.IngressPeer{
|
||||
Connected: true,
|
||||
Enabled: true,
|
||||
Id: "Test",
|
||||
}
|
||||
|
||||
func TestIngress_List_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/ingress/peers", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal([]api.IngressPeer{testIngressPeer})
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Ingress.List(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, ret, 1)
|
||||
assert.Equal(t, testIngressPeer, ret[0])
|
||||
})
|
||||
}
|
||||
|
||||
func TestIngress_List_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/ingress/peers", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
|
||||
w.WriteHeader(400)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Ingress.List(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "No", err.Error())
|
||||
assert.Empty(t, ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIngress_Get_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(testIngressPeer)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Ingress.Get(context.Background(), "Test")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testIngressPeer, *ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIngress_Get_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
|
||||
w.WriteHeader(400)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Ingress.Get(context.Background(), "Test")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "No", err.Error())
|
||||
assert.Empty(t, ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIngress_Create_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/ingress/peers", func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
reqBytes, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
var req api.PostApiIngressPeersJSONRequestBody
|
||||
err = json.Unmarshal(reqBytes, &req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "peer-id", req.PeerId)
|
||||
retBytes, _ := json.Marshal(testIngressPeer)
|
||||
_, err = w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Ingress.Create(context.Background(), api.PostApiIngressPeersJSONRequestBody{
|
||||
PeerId: "peer-id",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testIngressPeer, *ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIngress_Create_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/ingress/peers", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
|
||||
w.WriteHeader(400)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Ingress.Create(context.Background(), api.PostApiIngressPeersJSONRequestBody{
|
||||
PeerId: "peer-id",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "No", err.Error())
|
||||
assert.Nil(t, ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIngress_Update_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "PUT", r.Method)
|
||||
reqBytes, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
var req api.PutApiIngressPeersIngressPeerIdJSONRequestBody
|
||||
err = json.Unmarshal(reqBytes, &req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, true, req.Enabled)
|
||||
retBytes, _ := json.Marshal(testIngressPeer)
|
||||
_, err = w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Ingress.Update(context.Background(), "Test", api.PutApiIngressPeersIngressPeerIdJSONRequestBody{
|
||||
Enabled: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testIngressPeer, *ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIngress_Update_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
|
||||
w.WriteHeader(400)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Ingress.Update(context.Background(), "Test", api.PutApiIngressPeersIngressPeerIdJSONRequestBody{
|
||||
Enabled: true,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "No", err.Error())
|
||||
assert.Nil(t, ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIngress_Delete_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "DELETE", r.Method)
|
||||
w.WriteHeader(200)
|
||||
})
|
||||
err := c.Ingress.Delete(context.Background(), "Test")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIngress_Delete_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
|
||||
w.WriteHeader(404)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
err := c.Ingress.Delete(context.Background(), "Test")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Not found", err.Error())
|
||||
})
|
||||
}
|
||||
@@ -125,98 +125,6 @@ func (a *PeersAPI) CreateTemporaryAccess(ctx context.Context, peerID string, req
|
||||
return &ret, err
|
||||
}
|
||||
|
||||
// PeerIngressPortsAPI APIs for Peer Ingress Ports, do not use directly
|
||||
type PeerIngressPortsAPI struct {
|
||||
c *Client
|
||||
peerID string
|
||||
}
|
||||
|
||||
// IngressPorts APIs for peer ingress ports
|
||||
func (a *PeersAPI) IngressPorts(peerID string) *PeerIngressPortsAPI {
|
||||
return &PeerIngressPortsAPI{
|
||||
c: a.c,
|
||||
peerID: peerID,
|
||||
}
|
||||
}
|
||||
|
||||
// List list all ingress port allocations for a peer
|
||||
// See more: https://docs.netbird.io/api/resources/peers#list-all-ingress-port-allocations
|
||||
func (a *PeerIngressPortsAPI) List(ctx context.Context) ([]api.IngressPortAllocation, error) {
|
||||
resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+a.peerID+"/ingress/ports", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
ret, err := parseResponse[[]api.IngressPortAllocation](resp)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// Get get ingress port allocation info
|
||||
// See more: https://docs.netbird.io/api/resources/peers#retrieve-an-ingress-port-allocation
|
||||
func (a *PeerIngressPortsAPI) Get(ctx context.Context, allocationID string) (*api.IngressPortAllocation, error) {
|
||||
resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+a.peerID+"/ingress/ports/"+allocationID, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
ret, err := parseResponse[api.IngressPortAllocation](resp)
|
||||
return &ret, err
|
||||
}
|
||||
|
||||
// Create create new ingress port allocation
|
||||
// See more: https://docs.netbird.io/api/resources/peers#create-an-ingress-port-allocation
|
||||
func (a *PeerIngressPortsAPI) Create(ctx context.Context, request api.PostApiPeersPeerIdIngressPortsJSONRequestBody) (*api.IngressPortAllocation, error) {
|
||||
requestBytes, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := a.c.NewRequest(ctx, "POST", "/api/peers/"+a.peerID+"/ingress/ports", bytes.NewReader(requestBytes), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
ret, err := parseResponse[api.IngressPortAllocation](resp)
|
||||
return &ret, err
|
||||
}
|
||||
|
||||
// Update update ingress port allocation
|
||||
// See more: https://docs.netbird.io/api/resources/peers#update-an-ingress-port-allocation
|
||||
func (a *PeerIngressPortsAPI) Update(ctx context.Context, allocationID string, request api.PutApiPeersPeerIdIngressPortsAllocationIdJSONRequestBody) (*api.IngressPortAllocation, error) {
|
||||
requestBytes, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := a.c.NewRequest(ctx, "PUT", "/api/peers/"+a.peerID+"/ingress/ports/"+allocationID, bytes.NewReader(requestBytes), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
ret, err := parseResponse[api.IngressPortAllocation](resp)
|
||||
return &ret, err
|
||||
}
|
||||
|
||||
// Delete delete ingress port allocation
|
||||
// See more: https://docs.netbird.io/api/resources/peers#delete-an-ingress-port-allocation
|
||||
func (a *PeerIngressPortsAPI) Delete(ctx context.Context, allocationID string) error {
|
||||
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/peers/"+a.peerID+"/ingress/ports/"+allocationID, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PeerJobsAPI APIs for Peer Jobs, do not use directly
|
||||
type PeerJobsAPI struct {
|
||||
c *Client
|
||||
|
||||
@@ -31,11 +31,6 @@ var (
|
||||
Name: "test-peer",
|
||||
}
|
||||
|
||||
testIngressPortAllocation = api.IngressPortAllocation{
|
||||
Enabled: true,
|
||||
Id: "alloc-1",
|
||||
}
|
||||
|
||||
testJobResponse = api.JobResponse{
|
||||
Id: "job-1",
|
||||
Status: "pending",
|
||||
@@ -221,146 +216,6 @@ func TestPeers_CreateTemporaryAccess_Err(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeerIngressPorts_List_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/peers/Test/ingress/ports", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal([]api.IngressPortAllocation{testIngressPortAllocation})
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Peers.IngressPorts("Test").List(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, ret, 1)
|
||||
assert.Equal(t, testIngressPortAllocation, ret[0])
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeerIngressPorts_List_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/peers/Test/ingress/ports", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
|
||||
w.WriteHeader(400)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Peers.IngressPorts("Test").List(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "No", err.Error())
|
||||
assert.Empty(t, ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeerIngressPorts_Get_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(testIngressPortAllocation)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Peers.IngressPorts("Test").Get(context.Background(), "alloc-1")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testIngressPortAllocation, *ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeerIngressPorts_Get_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
|
||||
w.WriteHeader(400)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Peers.IngressPorts("Test").Get(context.Background(), "alloc-1")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "No", err.Error())
|
||||
assert.Empty(t, ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeerIngressPorts_Create_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/peers/Test/ingress/ports", func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
retBytes, _ := json.Marshal(testIngressPortAllocation)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Peers.IngressPorts("Test").Create(context.Background(), api.PostApiPeersPeerIdIngressPortsJSONRequestBody{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testIngressPortAllocation, *ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeerIngressPorts_Create_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/peers/Test/ingress/ports", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
|
||||
w.WriteHeader(400)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Peers.IngressPorts("Test").Create(context.Background(), api.PostApiPeersPeerIdIngressPortsJSONRequestBody{})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "No", err.Error())
|
||||
assert.Nil(t, ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeerIngressPorts_Update_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "PUT", r.Method)
|
||||
retBytes, _ := json.Marshal(testIngressPortAllocation)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Peers.IngressPorts("Test").Update(context.Background(), "alloc-1", api.PutApiPeersPeerIdIngressPortsAllocationIdJSONRequestBody{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testIngressPortAllocation, *ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeerIngressPorts_Update_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
|
||||
w.WriteHeader(400)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
ret, err := c.Peers.IngressPorts("Test").Update(context.Background(), "alloc-1", api.PutApiPeersPeerIdIngressPortsAllocationIdJSONRequestBody{})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "No", err.Error())
|
||||
assert.Nil(t, ret)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeerIngressPorts_Delete_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "DELETE", r.Method)
|
||||
w.WriteHeader(200)
|
||||
})
|
||||
err := c.Peers.IngressPorts("Test").Delete(context.Background(), "alloc-1")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeerIngressPorts_Delete_Err(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) {
|
||||
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
|
||||
w.WriteHeader(404)
|
||||
_, err := w.Write(retBytes)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
err := c.Peers.IngressPorts("Test").Delete(context.Background(), "alloc-1")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Not found", err.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeerJobs_List_200(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/peers/Test/jobs", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -31,9 +31,6 @@ tags:
|
||||
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
|
||||
@@ -383,6 +380,10 @@ components:
|
||||
description: Enables or disables client metrics push for all peers in the account
|
||||
type: boolean
|
||||
example: false
|
||||
peer_hostname_collision_rejected:
|
||||
description: When enabled, registering or renaming a peer to a hostname already used by another peer in the account fails instead of receiving an IP-based suffix. Applies to ephemeral peers as well.
|
||||
type: boolean
|
||||
example: false
|
||||
agent_network_only:
|
||||
description: Limits the dashboard to the Agent Network surface for this account. Set for accounts created via netbird.ai signups and can be disabled later. Enabling this requires dashboard_features.agent_network to be true in the same request.
|
||||
type: boolean
|
||||
@@ -826,6 +827,20 @@ components:
|
||||
- ssh_enabled
|
||||
- login_expiration_enabled
|
||||
- inactivity_expiration_enabled
|
||||
NetworkAddress:
|
||||
type: object
|
||||
properties:
|
||||
net_ip:
|
||||
description: IP address with CIDR of the interface
|
||||
type: string
|
||||
example: 192.168.0.11/24
|
||||
mac:
|
||||
description: MAC address of the interface
|
||||
type: string
|
||||
example: "00:93:37:bd:83:0f"
|
||||
required:
|
||||
- net_ip
|
||||
- mac
|
||||
Peer:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PeerMinimum'
|
||||
@@ -845,6 +860,11 @@ components:
|
||||
type: string
|
||||
format: ipv6
|
||||
example: "fd00:4e42:ab12::1"
|
||||
network_addresses:
|
||||
description: Network interfaces (IP + MAC) reported by the peer
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/NetworkAddress'
|
||||
connection_ip:
|
||||
description: Peer's public connection IP address
|
||||
type: string
|
||||
@@ -1688,6 +1708,8 @@ components:
|
||||
$ref: '#/components/schemas/PeerNetworkRangeCheck'
|
||||
process_check:
|
||||
$ref: '#/components/schemas/ProcessCheck'
|
||||
certificate_check:
|
||||
$ref: '#/components/schemas/CertificateCheck'
|
||||
NBVersionCheck:
|
||||
description: Posture check for the version of NetBird
|
||||
type: object
|
||||
@@ -1805,6 +1827,19 @@ components:
|
||||
description: Path to the process executable file in a Windows operating system
|
||||
type: string
|
||||
example: "C:\ProgramData\NetBird\netbird.exe"
|
||||
CertificateCheck:
|
||||
description: Posture check for a certificate held by the peer that chains to one of the given CA certificates. Not supported on mobile devices (iOS and Android), which always fail this check and are denied access
|
||||
type: object
|
||||
properties:
|
||||
ca_certificates:
|
||||
description: PEM encoded CA certificates the peer's certificate must chain to
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
type: string
|
||||
example: ["-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----"]
|
||||
required:
|
||||
- ca_certificates
|
||||
Location:
|
||||
description: Describe geographical location information
|
||||
type: object
|
||||
@@ -2433,222 +2468,6 @@ components:
|
||||
- initiator_email
|
||||
- target_id
|
||||
- meta
|
||||
IngressPeerCreateRequest:
|
||||
type: object
|
||||
properties:
|
||||
peer_id:
|
||||
description: ID of the peer that is used as an ingress peer
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7m0
|
||||
enabled:
|
||||
description: Defines if an ingress peer is enabled
|
||||
type: boolean
|
||||
example: true
|
||||
fallback:
|
||||
description: Defines if an ingress peer can be used as a fallback if no ingress peer can be found in the region of the forwarded peer
|
||||
type: boolean
|
||||
example: true
|
||||
required:
|
||||
- peer_id
|
||||
- enabled
|
||||
- fallback
|
||||
IngressPeerUpdateRequest:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
description: Defines if an ingress peer is enabled
|
||||
type: boolean
|
||||
example: true
|
||||
fallback:
|
||||
description: Defines if an ingress peer can be used as a fallback if no ingress peer can be found in the region of the forwarded peer
|
||||
type: boolean
|
||||
example: true
|
||||
required:
|
||||
- enabled
|
||||
- fallback
|
||||
IngressPeer:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: ID of the ingress peer
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7m0
|
||||
peer_id:
|
||||
description: ID of the peer that is used as an ingress peer
|
||||
type: string
|
||||
example: x7p3kqf2rdd8j5zxw4n9
|
||||
ingress_ip:
|
||||
description: Ingress IP address of the ingress peer where the traffic arrives
|
||||
type: string
|
||||
example: 192.34.0.123
|
||||
available_ports:
|
||||
$ref: '#/components/schemas/AvailablePorts'
|
||||
enabled:
|
||||
description: Indicates if an ingress peer is enabled
|
||||
type: boolean
|
||||
example: true
|
||||
connected:
|
||||
description: Indicates if an ingress peer is connected to the management server
|
||||
type: boolean
|
||||
example: true
|
||||
fallback:
|
||||
description: Indicates if an ingress peer can be used as a fallback if no ingress peer can be found in the region of the forwarded peer
|
||||
type: boolean
|
||||
example: true
|
||||
region:
|
||||
description: Region of the ingress peer
|
||||
type: string
|
||||
example: germany
|
||||
required:
|
||||
- id
|
||||
- peer_id
|
||||
- ingress_ip
|
||||
- available_ports
|
||||
- enabled
|
||||
- connected
|
||||
- fallback
|
||||
- region
|
||||
AvailablePorts:
|
||||
type: object
|
||||
properties:
|
||||
tcp:
|
||||
description: Number of available TCP ports left on the ingress peer
|
||||
type: integer
|
||||
example: 45765
|
||||
udp:
|
||||
description: Number of available UDP ports left on the ingress peer
|
||||
type: integer
|
||||
example: 50000
|
||||
required:
|
||||
- tcp
|
||||
- udp
|
||||
IngressPortAllocationRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
description: Name of the ingress port allocation
|
||||
type: string
|
||||
example: Ingress Port Allocation 1
|
||||
enabled:
|
||||
description: Indicates if an ingress port allocation is enabled
|
||||
type: boolean
|
||||
example: true
|
||||
port_ranges:
|
||||
description: List of port ranges that are forwarded by the ingress peer
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/IngressPortAllocationRequestPortRange'
|
||||
direct_port:
|
||||
description: Direct port allocation
|
||||
$ref: '#/components/schemas/IngressPortAllocationRequestDirectPort'
|
||||
required:
|
||||
- name
|
||||
- enabled
|
||||
IngressPortAllocationRequestPortRange:
|
||||
type: object
|
||||
properties:
|
||||
start:
|
||||
description: The starting port of the range of forwarded ports
|
||||
type: integer
|
||||
example: 80
|
||||
end:
|
||||
description: The ending port of the range of forwarded ports
|
||||
type: integer
|
||||
example: 320
|
||||
protocol:
|
||||
description: The protocol accepted by the port range
|
||||
type: string
|
||||
enum: [ "tcp", "udp", "tcp/udp" ]
|
||||
example: tcp
|
||||
required:
|
||||
- start
|
||||
- end
|
||||
- protocol
|
||||
IngressPortAllocationRequestDirectPort:
|
||||
type: object
|
||||
properties:
|
||||
count:
|
||||
description: The number of ports to be forwarded
|
||||
type: integer
|
||||
example: 5
|
||||
protocol:
|
||||
description: The protocol accepted by the port
|
||||
type: string
|
||||
enum: [ "tcp", "udp", "tcp/udp" ]
|
||||
example: udp
|
||||
required:
|
||||
- count
|
||||
- protocol
|
||||
IngressPortAllocation:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: ID of the ingress port allocation
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7m0
|
||||
name:
|
||||
description: Name of the ingress port allocation
|
||||
type: string
|
||||
example: Ingress Peer Allocation 1
|
||||
ingress_peer_id:
|
||||
description: ID of the ingress peer that forwards the ports
|
||||
type: string
|
||||
example: x7p3kqf2rdd8j5zxw4n9
|
||||
region:
|
||||
description: Region of the ingress peer
|
||||
type: string
|
||||
example: germany
|
||||
enabled:
|
||||
description: Indicates if an ingress port allocation is enabled
|
||||
type: boolean
|
||||
example: true
|
||||
ingress_ip:
|
||||
description: Ingress IP address of the ingress peer where the traffic arrives
|
||||
type: string
|
||||
example: 192.34.0.123
|
||||
port_range_mappings:
|
||||
description: List of port ranges that are allowed to be used by the ingress peer
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/IngressPortAllocationPortMapping'
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- ingress_peer_id
|
||||
- region
|
||||
- enabled
|
||||
- ingress_ip
|
||||
- port_range_mappings
|
||||
IngressPortAllocationPortMapping:
|
||||
type: object
|
||||
properties:
|
||||
translated_start:
|
||||
description: The starting port of the translated range of forwarded ports
|
||||
type: integer
|
||||
example: 80
|
||||
translated_end:
|
||||
description: The ending port of the translated range of forwarded ports
|
||||
type: integer
|
||||
example: 320
|
||||
ingress_start:
|
||||
description: The starting port of the range of ingress ports mapped to the forwarded ports
|
||||
type: integer
|
||||
example: 1080
|
||||
ingress_end:
|
||||
description: The ending port of the range of ingress ports mapped to the forwarded ports
|
||||
type: integer
|
||||
example: 1320
|
||||
protocol:
|
||||
description: Protocol accepted by the ports
|
||||
type: string
|
||||
enum: [ "tcp", "udp", "tcp/udp" ]
|
||||
example: tcp
|
||||
required:
|
||||
- translated_start
|
||||
- translated_end
|
||||
- ingress_start
|
||||
- ingress_end
|
||||
- protocol
|
||||
NetworkTrafficLocation:
|
||||
type: object
|
||||
properties:
|
||||
@@ -3303,6 +3122,12 @@ components:
|
||||
type: string
|
||||
description: URL path prefix for this target (HTTP only)
|
||||
example: "/"
|
||||
access_action:
|
||||
type: string
|
||||
description: Access action applied after this target's HTTP path prefix is selected. "inherit" uses the service authentication configuration, "bypass" skips service authentication, and "block" denies access. HTTP services only.
|
||||
enum: [inherit, bypass, block]
|
||||
default: inherit
|
||||
example: "inherit"
|
||||
protocol:
|
||||
type: string
|
||||
description: Protocol to use when connecting to the backend
|
||||
@@ -6466,8 +6291,8 @@ components:
|
||||
example: "d1m3kebd9pcs0c1pnu7g"
|
||||
state:
|
||||
type: string
|
||||
description: Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure.
|
||||
enum: [ "provisioning", "ready", "failed" ]
|
||||
description: Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure, `disabled` while the gateway is turned off and the endpoint is not served.
|
||||
enum: [ "provisioning", "ready", "failed", "disabled" ]
|
||||
example: "ready"
|
||||
endpoint:
|
||||
type: string
|
||||
@@ -7529,6 +7354,11 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
description: Filter peers by IP address
|
||||
- in: query
|
||||
name: mac
|
||||
schema:
|
||||
type: string
|
||||
description: Filter peers by MAC address of a network interface
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
@@ -7708,341 +7538,6 @@ paths:
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/peers/{peerId}/ingress/ports:
|
||||
get:
|
||||
x-cloud-only: true
|
||||
summary: List all Port Allocations
|
||||
description: Returns a list of all ingress port allocations for a peer
|
||||
tags: [ Ingress Ports ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: peerId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a peer
|
||||
- in: query
|
||||
name: name
|
||||
schema:
|
||||
type: string
|
||||
description: Filters ingress port allocations by name
|
||||
responses:
|
||||
'200':
|
||||
description: A JSON Array of Ingress Port Allocations
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/IngressPortAllocation'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
post:
|
||||
x-cloud-only: true
|
||||
summary: Create a Port Allocation
|
||||
description: Creates a new ingress port allocation for a peer
|
||||
tags: [ Ingress Ports ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: peerId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a peer
|
||||
requestBody:
|
||||
description: New Ingress Port Allocation request
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/IngressPortAllocationRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A Ingress Port Allocation object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IngressPortAllocation'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/peers/{peerId}/ingress/ports/{allocationId}:
|
||||
get:
|
||||
x-cloud-only: true
|
||||
summary: Retrieve a Port Allocation
|
||||
description: Get information about an ingress port allocation
|
||||
tags: [ Ingress Ports ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: peerId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a peer
|
||||
- in: path
|
||||
name: allocationId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of an ingress port allocation
|
||||
responses:
|
||||
'200':
|
||||
description: A Ingress Port Allocation object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IngressPortAllocation'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
put:
|
||||
x-cloud-only: true
|
||||
summary: Update a Port Allocation
|
||||
description: Update information about an ingress port allocation
|
||||
tags: [ Ingress Ports ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: peerId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a peer
|
||||
- in: path
|
||||
name: allocationId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of an ingress port allocation
|
||||
requestBody:
|
||||
description: update an ingress port allocation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IngressPortAllocationRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A Ingress Port Allocation object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IngressPortAllocation'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
delete:
|
||||
x-cloud-only: true
|
||||
summary: Delete a Port Allocation
|
||||
description: Delete an ingress port allocation
|
||||
tags: [ Ingress Ports ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: peerId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a peer
|
||||
- in: path
|
||||
name: allocationId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of an ingress port allocation
|
||||
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"
|
||||
/api/ingress/peers:
|
||||
get:
|
||||
x-cloud-only: true
|
||||
summary: List all Ingress Peers
|
||||
description: Returns a list of all ingress peers
|
||||
tags: [ Ingress Ports ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
responses:
|
||||
'200':
|
||||
description: A JSON Array of Ingress Peers
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/IngressPeer'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
post:
|
||||
x-cloud-only: true
|
||||
summary: Create a Ingress Peer
|
||||
description: Creates a new ingress peer
|
||||
tags: [ Ingress Ports ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
requestBody:
|
||||
description: New Ingress Peer request
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/IngressPeerCreateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A Ingress Peer object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IngressPeer'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/ingress/peers/{ingressPeerId}:
|
||||
get:
|
||||
x-cloud-only: true
|
||||
summary: Retrieve a Ingress Peer
|
||||
description: Get information about an ingress peer
|
||||
tags: [ Ingress Ports ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: ingressPeerId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of an ingress peer
|
||||
responses:
|
||||
'200':
|
||||
description: A Ingress Peer object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IngressPeer'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
put:
|
||||
x-cloud-only: true
|
||||
summary: Update a Ingress Peer
|
||||
description: Update information about an ingress peer
|
||||
tags: [ Ingress Ports ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: ingressPeerId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of an ingress peer
|
||||
requestBody:
|
||||
description: update an ingress peer
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/IngressPeerUpdateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A Ingress Peer object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IngressPeer'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
delete:
|
||||
x-cloud-only: true
|
||||
summary: Delete a Ingress Peer
|
||||
description: Delete an ingress peer
|
||||
tags: [ Ingress Ports ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: ingressPeerId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of an ingress 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"
|
||||
/api/setup-keys:
|
||||
get:
|
||||
summary: List all Setup Keys
|
||||
@@ -10614,6 +10109,28 @@ paths:
|
||||
$ref: "#/components/responses/requires_authentication"
|
||||
"500":
|
||||
$ref: "#/components/responses/internal_error"
|
||||
delete:
|
||||
summary: Delete MSP tenant
|
||||
tags:
|
||||
- MSP
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of a tenant account
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully deleted the tenant
|
||||
"400":
|
||||
$ref: "#/components/responses/bad_request"
|
||||
"403":
|
||||
$ref: "#/components/responses/requires_authentication"
|
||||
"404":
|
||||
description: The tenant was not found
|
||||
"500":
|
||||
$ref: "#/components/responses/internal_error"
|
||||
/api/integrations/msp/tenants/{id}/unlink:
|
||||
post:
|
||||
summary: Unlink a tenant
|
||||
@@ -13360,7 +12877,7 @@ paths:
|
||||
/api/reverse-proxies/domains/{domainId}:
|
||||
delete:
|
||||
summary: Delete a Custom domain
|
||||
description: Delete an existing service custom domain
|
||||
description: Delete an existing service custom domain after removing or moving all services that use it or its subdomains, including disabled services.
|
||||
tags: [ Services ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
@@ -13383,6 +12900,9 @@ paths:
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'404':
|
||||
"$ref": "#/components/responses/not_found"
|
||||
'412':
|
||||
description: The domain or one of its subdomains is still used by a service
|
||||
content: { }
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/reverse-proxies/domains/{domainId}/validate:
|
||||
@@ -13585,7 +13105,7 @@ paths:
|
||||
/api/integrations/agent-network/managed-proxy:
|
||||
post:
|
||||
summary: Provision a managed Agent Network gateway
|
||||
description: Starts provisioning of a NetBird-managed Agent Network gateway for the account, allocating its endpoint under the managed zone on the first call. Idempotent — answers 202 when this call started (or, after a failure, restarted) provisioning and 200 when a deployment already exists, reporting current state either way. Returns 409 when the account already has an Agent Network endpoint that managed provisioning does not own, and 503 when endpoint allocation is temporarily exhausted.
|
||||
description: Starts provisioning of a NetBird-managed Agent Network gateway for the account, allocating its endpoint under the managed zone on the first call. Idempotent — answers 202 when this call started (or, after a failure, restarted) provisioning and 200 when a deployment already exists, reporting current state either way. A disabled deployment answers 200 with state `disabled` and stays disabled. Returns 409 when the account already has an Agent Network endpoint that managed provisioning does not own, and 503 when endpoint allocation is temporarily exhausted.
|
||||
tags: [ Agent Network ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
@@ -13623,7 +13143,7 @@ paths:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
get:
|
||||
summary: Retrieve managed Agent Network gateway status
|
||||
description: Reports the account's managed gateway deployment and its derived state. Returns 404 when the account has no managed deployment.
|
||||
description: Reports the account's managed gateway deployment and its derived state, including `disabled` for a deployment that is turned off. Returns 404 when the account has no managed deployment.
|
||||
tags: [ Agent Network ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
|
||||
@@ -79,6 +79,7 @@ func (e AgentNetworkConsumptionDimensionKind) Valid() bool {
|
||||
|
||||
// Defines values for AgentNetworkManagedProxyState.
|
||||
const (
|
||||
AgentNetworkManagedProxyStateDisabled AgentNetworkManagedProxyState = "disabled"
|
||||
AgentNetworkManagedProxyStateFailed AgentNetworkManagedProxyState = "failed"
|
||||
AgentNetworkManagedProxyStateProvisioning AgentNetworkManagedProxyState = "provisioning"
|
||||
AgentNetworkManagedProxyStateReady AgentNetworkManagedProxyState = "ready"
|
||||
@@ -87,6 +88,8 @@ const (
|
||||
// Valid indicates whether the value is a known member of the AgentNetworkManagedProxyState enum.
|
||||
func (e AgentNetworkManagedProxyState) Valid() bool {
|
||||
switch e {
|
||||
case AgentNetworkManagedProxyStateDisabled:
|
||||
return true
|
||||
case AgentNetworkManagedProxyStateFailed:
|
||||
return true
|
||||
case AgentNetworkManagedProxyStateProvisioning:
|
||||
@@ -605,69 +608,6 @@ func (e IdentityProviderType) Valid() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for IngressPortAllocationPortMappingProtocol.
|
||||
const (
|
||||
IngressPortAllocationPortMappingProtocolTcp IngressPortAllocationPortMappingProtocol = "tcp"
|
||||
IngressPortAllocationPortMappingProtocolTcpudp IngressPortAllocationPortMappingProtocol = "tcp/udp"
|
||||
IngressPortAllocationPortMappingProtocolUdp IngressPortAllocationPortMappingProtocol = "udp"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the IngressPortAllocationPortMappingProtocol enum.
|
||||
func (e IngressPortAllocationPortMappingProtocol) Valid() bool {
|
||||
switch e {
|
||||
case IngressPortAllocationPortMappingProtocolTcp:
|
||||
return true
|
||||
case IngressPortAllocationPortMappingProtocolTcpudp:
|
||||
return true
|
||||
case IngressPortAllocationPortMappingProtocolUdp:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for IngressPortAllocationRequestDirectPortProtocol.
|
||||
const (
|
||||
IngressPortAllocationRequestDirectPortProtocolTcp IngressPortAllocationRequestDirectPortProtocol = "tcp"
|
||||
IngressPortAllocationRequestDirectPortProtocolTcpudp IngressPortAllocationRequestDirectPortProtocol = "tcp/udp"
|
||||
IngressPortAllocationRequestDirectPortProtocolUdp IngressPortAllocationRequestDirectPortProtocol = "udp"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the IngressPortAllocationRequestDirectPortProtocol enum.
|
||||
func (e IngressPortAllocationRequestDirectPortProtocol) Valid() bool {
|
||||
switch e {
|
||||
case IngressPortAllocationRequestDirectPortProtocolTcp:
|
||||
return true
|
||||
case IngressPortAllocationRequestDirectPortProtocolTcpudp:
|
||||
return true
|
||||
case IngressPortAllocationRequestDirectPortProtocolUdp:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for IngressPortAllocationRequestPortRangeProtocol.
|
||||
const (
|
||||
IngressPortAllocationRequestPortRangeProtocolTcp IngressPortAllocationRequestPortRangeProtocol = "tcp"
|
||||
IngressPortAllocationRequestPortRangeProtocolTcpudp IngressPortAllocationRequestPortRangeProtocol = "tcp/udp"
|
||||
IngressPortAllocationRequestPortRangeProtocolUdp IngressPortAllocationRequestPortRangeProtocol = "udp"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the IngressPortAllocationRequestPortRangeProtocol enum.
|
||||
func (e IngressPortAllocationRequestPortRangeProtocol) Valid() bool {
|
||||
switch e {
|
||||
case IngressPortAllocationRequestPortRangeProtocolTcp:
|
||||
return true
|
||||
case IngressPortAllocationRequestPortRangeProtocolTcpudp:
|
||||
return true
|
||||
case IngressPortAllocationRequestPortRangeProtocolUdp:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for IntegrationResponsePlatform.
|
||||
const (
|
||||
IntegrationResponsePlatformDatadog IntegrationResponsePlatform = "datadog"
|
||||
@@ -1097,6 +1037,27 @@ func (e ServiceRequestMode) Valid() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for ServiceTargetAccessAction.
|
||||
const (
|
||||
ServiceTargetAccessActionBlock ServiceTargetAccessAction = "block"
|
||||
ServiceTargetAccessActionBypass ServiceTargetAccessAction = "bypass"
|
||||
ServiceTargetAccessActionInherit ServiceTargetAccessAction = "inherit"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the ServiceTargetAccessAction enum.
|
||||
func (e ServiceTargetAccessAction) Valid() bool {
|
||||
switch e {
|
||||
case ServiceTargetAccessActionBlock:
|
||||
return true
|
||||
case ServiceTargetAccessActionBypass:
|
||||
return true
|
||||
case ServiceTargetAccessActionInherit:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for ServiceTargetProtocol.
|
||||
const (
|
||||
ServiceTargetProtocolHttp ServiceTargetProtocol = "http"
|
||||
@@ -1732,6 +1693,9 @@ type AccountSettings struct {
|
||||
// PeerExposeGroups Limits which peer groups are allowed to expose services. If empty, all peers are allowed when peer expose is enabled.
|
||||
PeerExposeGroups []string `json:"peer_expose_groups"`
|
||||
|
||||
// PeerHostnameCollisionRejected When enabled, registering or renaming a peer to a hostname already used by another peer in the account fails instead of receiving an IP-based suffix. Applies to ephemeral peers as well.
|
||||
PeerHostnameCollisionRejected *bool `json:"peer_hostname_collision_rejected,omitempty"`
|
||||
|
||||
// PeerInactivityExpiration Period of time of inactivity after which peer session expires (seconds).
|
||||
PeerInactivityExpiration int `json:"peer_inactivity_expiration"`
|
||||
|
||||
@@ -2259,11 +2223,11 @@ type AgentNetworkManagedProxy struct {
|
||||
// Region Region of the cluster hosting the deployment.
|
||||
Region *string `json:"region,omitempty"`
|
||||
|
||||
// State Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure.
|
||||
// State Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure, `disabled` while the gateway is turned off and the endpoint is not served.
|
||||
State AgentNetworkManagedProxyState `json:"state"`
|
||||
}
|
||||
|
||||
// AgentNetworkManagedProxyState Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure.
|
||||
// AgentNetworkManagedProxyState Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure, `disabled` while the gateway is turned off and the endpoint is not served.
|
||||
type AgentNetworkManagedProxyState string
|
||||
|
||||
// AgentNetworkManagedProxyConflict Conflict body returned when the account already has an Agent Network endpoint that managed provisioning does not own, naming that endpoint.
|
||||
@@ -2597,15 +2561,6 @@ type AgentNetworkUsageBucket struct {
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
}
|
||||
|
||||
// AvailablePorts defines model for AvailablePorts.
|
||||
type AvailablePorts struct {
|
||||
// Tcp Number of available TCP ports left on the ingress peer
|
||||
Tcp int `json:"tcp"`
|
||||
|
||||
// Udp Number of available UDP ports left on the ingress peer
|
||||
Udp int `json:"udp"`
|
||||
}
|
||||
|
||||
// AzureIntegration defines model for AzureIntegration.
|
||||
type AzureIntegration struct {
|
||||
// ClientId Azure AD application (client) ID
|
||||
@@ -2701,6 +2656,12 @@ type BypassResponse struct {
|
||||
PeerId string `json:"peer_id"`
|
||||
}
|
||||
|
||||
// CertificateCheck Posture check for a certificate held by the peer that chains to one of the given CA certificates. Not supported on mobile devices (iOS and Android), which always fail this check and are denied access
|
||||
type CertificateCheck struct {
|
||||
// CaCertificates PEM encoded CA certificates the peer's certificate must chain to
|
||||
CaCertificates []string `json:"ca_certificates"`
|
||||
}
|
||||
|
||||
// CheckoutResponse defines model for CheckoutResponse.
|
||||
type CheckoutResponse struct {
|
||||
// SessionId The unique identifier for the checkout session.
|
||||
@@ -2712,6 +2673,9 @@ type CheckoutResponse struct {
|
||||
|
||||
// Checks List of objects that perform the actual checks
|
||||
type Checks struct {
|
||||
// CertificateCheck Posture check for a certificate held by the peer that chains to one of the given CA certificates. Not supported on mobile devices (iOS and Android), which always fail this check and are denied access
|
||||
CertificateCheck *CertificateCheck `json:"certificate_check,omitempty"`
|
||||
|
||||
// GeoLocationCheck Posture check for geo location
|
||||
GeoLocationCheck *GeoLocationCheck `json:"geo_location_check,omitempty"`
|
||||
|
||||
@@ -3458,139 +3422,6 @@ type IdpIntegrationSyncLog struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// IngressPeer defines model for IngressPeer.
|
||||
type IngressPeer struct {
|
||||
AvailablePorts AvailablePorts `json:"available_ports"`
|
||||
|
||||
// Connected Indicates if an ingress peer is connected to the management server
|
||||
Connected bool `json:"connected"`
|
||||
|
||||
// Enabled Indicates if an ingress peer is enabled
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Fallback Indicates if an ingress peer can be used as a fallback if no ingress peer can be found in the region of the forwarded peer
|
||||
Fallback bool `json:"fallback"`
|
||||
|
||||
// Id ID of the ingress peer
|
||||
Id string `json:"id"`
|
||||
|
||||
// IngressIp Ingress IP address of the ingress peer where the traffic arrives
|
||||
IngressIp string `json:"ingress_ip"`
|
||||
|
||||
// PeerId ID of the peer that is used as an ingress peer
|
||||
PeerId string `json:"peer_id"`
|
||||
|
||||
// Region Region of the ingress peer
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
// IngressPeerCreateRequest defines model for IngressPeerCreateRequest.
|
||||
type IngressPeerCreateRequest struct {
|
||||
// Enabled Defines if an ingress peer is enabled
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Fallback Defines if an ingress peer can be used as a fallback if no ingress peer can be found in the region of the forwarded peer
|
||||
Fallback bool `json:"fallback"`
|
||||
|
||||
// PeerId ID of the peer that is used as an ingress peer
|
||||
PeerId string `json:"peer_id"`
|
||||
}
|
||||
|
||||
// IngressPeerUpdateRequest defines model for IngressPeerUpdateRequest.
|
||||
type IngressPeerUpdateRequest struct {
|
||||
// Enabled Defines if an ingress peer is enabled
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Fallback Defines if an ingress peer can be used as a fallback if no ingress peer can be found in the region of the forwarded peer
|
||||
Fallback bool `json:"fallback"`
|
||||
}
|
||||
|
||||
// IngressPortAllocation defines model for IngressPortAllocation.
|
||||
type IngressPortAllocation struct {
|
||||
// Enabled Indicates if an ingress port allocation is enabled
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Id ID of the ingress port allocation
|
||||
Id string `json:"id"`
|
||||
|
||||
// IngressIp Ingress IP address of the ingress peer where the traffic arrives
|
||||
IngressIp string `json:"ingress_ip"`
|
||||
|
||||
// IngressPeerId ID of the ingress peer that forwards the ports
|
||||
IngressPeerId string `json:"ingress_peer_id"`
|
||||
|
||||
// Name Name of the ingress port allocation
|
||||
Name string `json:"name"`
|
||||
|
||||
// PortRangeMappings List of port ranges that are allowed to be used by the ingress peer
|
||||
PortRangeMappings []IngressPortAllocationPortMapping `json:"port_range_mappings"`
|
||||
|
||||
// Region Region of the ingress peer
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
// IngressPortAllocationPortMapping defines model for IngressPortAllocationPortMapping.
|
||||
type IngressPortAllocationPortMapping struct {
|
||||
// IngressEnd The ending port of the range of ingress ports mapped to the forwarded ports
|
||||
IngressEnd int `json:"ingress_end"`
|
||||
|
||||
// IngressStart The starting port of the range of ingress ports mapped to the forwarded ports
|
||||
IngressStart int `json:"ingress_start"`
|
||||
|
||||
// Protocol Protocol accepted by the ports
|
||||
Protocol IngressPortAllocationPortMappingProtocol `json:"protocol"`
|
||||
|
||||
// TranslatedEnd The ending port of the translated range of forwarded ports
|
||||
TranslatedEnd int `json:"translated_end"`
|
||||
|
||||
// TranslatedStart The starting port of the translated range of forwarded ports
|
||||
TranslatedStart int `json:"translated_start"`
|
||||
}
|
||||
|
||||
// IngressPortAllocationPortMappingProtocol Protocol accepted by the ports
|
||||
type IngressPortAllocationPortMappingProtocol string
|
||||
|
||||
// IngressPortAllocationRequest defines model for IngressPortAllocationRequest.
|
||||
type IngressPortAllocationRequest struct {
|
||||
DirectPort *IngressPortAllocationRequestDirectPort `json:"direct_port,omitempty"`
|
||||
|
||||
// Enabled Indicates if an ingress port allocation is enabled
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Name Name of the ingress port allocation
|
||||
Name string `json:"name"`
|
||||
|
||||
// PortRanges List of port ranges that are forwarded by the ingress peer
|
||||
PortRanges *[]IngressPortAllocationRequestPortRange `json:"port_ranges,omitempty"`
|
||||
}
|
||||
|
||||
// IngressPortAllocationRequestDirectPort defines model for IngressPortAllocationRequestDirectPort.
|
||||
type IngressPortAllocationRequestDirectPort struct {
|
||||
// Count The number of ports to be forwarded
|
||||
Count int `json:"count"`
|
||||
|
||||
// Protocol The protocol accepted by the port
|
||||
Protocol IngressPortAllocationRequestDirectPortProtocol `json:"protocol"`
|
||||
}
|
||||
|
||||
// IngressPortAllocationRequestDirectPortProtocol The protocol accepted by the port
|
||||
type IngressPortAllocationRequestDirectPortProtocol string
|
||||
|
||||
// IngressPortAllocationRequestPortRange defines model for IngressPortAllocationRequestPortRange.
|
||||
type IngressPortAllocationRequestPortRange struct {
|
||||
// End The ending port of the range of forwarded ports
|
||||
End int `json:"end"`
|
||||
|
||||
// Protocol The protocol accepted by the port range
|
||||
Protocol IngressPortAllocationRequestPortRangeProtocol `json:"protocol"`
|
||||
|
||||
// Start The starting port of the range of forwarded ports
|
||||
Start int `json:"start"`
|
||||
}
|
||||
|
||||
// IngressPortAllocationRequestPortRangeProtocol The protocol accepted by the port range
|
||||
type IngressPortAllocationRequestPortRangeProtocol string
|
||||
|
||||
// InstanceStatus Instance status information
|
||||
type InstanceStatus struct {
|
||||
// SetupRequired Indicates whether the instance requires initial setup
|
||||
@@ -3826,6 +3657,15 @@ type Network struct {
|
||||
RoutingPeersCount int `json:"routing_peers_count"`
|
||||
}
|
||||
|
||||
// NetworkAddress defines model for NetworkAddress.
|
||||
type NetworkAddress struct {
|
||||
// Mac MAC address of the interface
|
||||
Mac string `json:"mac"`
|
||||
|
||||
// NetIp IP address with CIDR of the interface
|
||||
NetIp string `json:"net_ip"`
|
||||
}
|
||||
|
||||
// NetworkRequest defines model for NetworkRequest.
|
||||
type NetworkRequest struct {
|
||||
// Description Network description
|
||||
@@ -4275,6 +4115,9 @@ type Peer struct {
|
||||
// Name Peer's hostname
|
||||
Name string `json:"name"`
|
||||
|
||||
// NetworkAddresses Network interfaces (IP + MAC) reported by the peer
|
||||
NetworkAddresses *[]NetworkAddress `json:"network_addresses,omitempty"`
|
||||
|
||||
// Os Peer's operating system and version
|
||||
Os string `json:"os"`
|
||||
|
||||
@@ -4369,6 +4212,9 @@ type PeerBatch struct {
|
||||
// Name Peer's hostname
|
||||
Name string `json:"name"`
|
||||
|
||||
// NetworkAddresses Network interfaces (IP + MAC) reported by the peer
|
||||
NetworkAddresses *[]NetworkAddress `json:"network_addresses,omitempty"`
|
||||
|
||||
// Os Peer's operating system and version
|
||||
Os string `json:"os"`
|
||||
|
||||
@@ -5300,6 +5146,9 @@ type ServiceRequestMode string
|
||||
|
||||
// ServiceTarget defines model for ServiceTarget.
|
||||
type ServiceTarget struct {
|
||||
// AccessAction Access action applied after this target's HTTP path prefix is selected. "inherit" uses the service authentication configuration, "bypass" skips service authentication, and "block" denies access. HTTP services only.
|
||||
AccessAction *ServiceTargetAccessAction `json:"access_action,omitempty"`
|
||||
|
||||
// Enabled Whether this target is enabled
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
@@ -5323,6 +5172,9 @@ type ServiceTarget struct {
|
||||
TargetType ServiceTargetTargetType `json:"target_type"`
|
||||
}
|
||||
|
||||
// ServiceTargetAccessAction Access action applied after this target's HTTP path prefix is selected. "inherit" uses the service authentication configuration, "bypass" skips service authentication, and "block" denies access. HTTP services only.
|
||||
type ServiceTargetAccessAction string
|
||||
|
||||
// ServiceTargetProtocol Protocol to use when connecting to the backend
|
||||
type ServiceTargetProtocol string
|
||||
|
||||
@@ -6300,12 +6152,9 @@ type GetApiPeersParams struct {
|
||||
|
||||
// Ip Filter peers by IP address
|
||||
Ip *string `form:"ip,omitempty" json:"ip,omitempty"`
|
||||
}
|
||||
|
||||
// GetApiPeersPeerIdIngressPortsParams defines parameters for GetApiPeersPeerIdIngressPorts.
|
||||
type GetApiPeersPeerIdIngressPortsParams struct {
|
||||
// Name Filters ingress port allocations by name
|
||||
Name *string `form:"name,omitempty" json:"name,omitempty"`
|
||||
// Mac Filter peers by MAC address of a network interface
|
||||
Mac *string `form:"mac,omitempty" json:"mac,omitempty"`
|
||||
}
|
||||
|
||||
// GetApiUsersParams defines parameters for GetApiUsers.
|
||||
@@ -6389,12 +6238,6 @@ type PostApiIdentityProvidersJSONRequestBody = IdentityProviderRequest
|
||||
// PutApiIdentityProvidersIdpIdJSONRequestBody defines body for PutApiIdentityProvidersIdpId for application/json ContentType.
|
||||
type PutApiIdentityProvidersIdpIdJSONRequestBody = IdentityProviderRequest
|
||||
|
||||
// PostApiIngressPeersJSONRequestBody defines body for PostApiIngressPeers for application/json ContentType.
|
||||
type PostApiIngressPeersJSONRequestBody = IngressPeerCreateRequest
|
||||
|
||||
// PutApiIngressPeersIngressPeerIdJSONRequestBody defines body for PutApiIngressPeersIngressPeerId for application/json ContentType.
|
||||
type PutApiIngressPeersIngressPeerIdJSONRequestBody = IngressPeerUpdateRequest
|
||||
|
||||
// CreateAzureIntegrationJSONRequestBody defines body for CreateAzureIntegration for application/json ContentType.
|
||||
type CreateAzureIntegrationJSONRequestBody = CreateAzureIntegrationRequest
|
||||
|
||||
@@ -6503,12 +6346,6 @@ type PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody = NetworkRouterReques
|
||||
// PutApiPeersPeerIdJSONRequestBody defines body for PutApiPeersPeerId for application/json ContentType.
|
||||
type PutApiPeersPeerIdJSONRequestBody = PeerRequest
|
||||
|
||||
// PostApiPeersPeerIdIngressPortsJSONRequestBody defines body for PostApiPeersPeerIdIngressPorts for application/json ContentType.
|
||||
type PostApiPeersPeerIdIngressPortsJSONRequestBody = IngressPortAllocationRequest
|
||||
|
||||
// PutApiPeersPeerIdIngressPortsAllocationIdJSONRequestBody defines body for PutApiPeersPeerIdIngressPortsAllocationId for application/json ContentType.
|
||||
type PutApiPeersPeerIdIngressPortsAllocationIdJSONRequestBody = IngressPortAllocationRequest
|
||||
|
||||
// PostApiPeersPeerIdJobsJSONRequestBody defines body for PostApiPeersPeerIdJobs for application/json ContentType.
|
||||
type PostApiPeersPeerIdJobsJSONRequestBody = JobRequest
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// - NetworkMap is the *proto.NetworkMap shape the engine reads today via
|
||||
// update.GetNetworkMap() — built from the envelope's components by
|
||||
// running Calculate() locally + converting back through the shared
|
||||
// proto helpers + merging the optional ProxyPatch.
|
||||
// proto helpers.
|
||||
// - Components is the *types.NetworkMapComponents the engine retains so
|
||||
// future incremental delta updates have a base to apply changes
|
||||
// against. The client keeps it under its sync lock.
|
||||
@@ -26,8 +26,8 @@ type EnvelopeResult struct {
|
||||
|
||||
// EnvelopeToNetworkMap is the full client-side pipeline: decode the
|
||||
// component envelope back to a typed NetworkMapComponents, run Calculate()
|
||||
// locally to produce the typed NetworkMap, convert it to the wire form the
|
||||
// engine consumes, and fold in any ProxyPatch the server attached.
|
||||
// locally to produce the typed NetworkMap and convert it to the wire form the
|
||||
// engine consumes.
|
||||
//
|
||||
// localPeerKey is the receiving peer's WG pub key (used to derive
|
||||
// includeIPv6 / useSourcePrefixes from the receiving peer's own record in
|
||||
@@ -128,74 +128,12 @@ func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, lo
|
||||
}
|
||||
}
|
||||
|
||||
if typedNM.ForwardingRules != nil {
|
||||
forwardingRules := make([]*proto.ForwardingRule, 0, len(typedNM.ForwardingRules))
|
||||
for _, rule := range typedNM.ForwardingRules {
|
||||
forwardingRules = append(forwardingRules, rule.ToProto())
|
||||
}
|
||||
protoNM.ForwardingRules = forwardingRules
|
||||
}
|
||||
|
||||
// Merge the proxy patch the server attached. Mirrors the legacy
|
||||
// NetworkMap.Merge step that the server runs after Calculate().
|
||||
if full != nil && full.ProxyPatch != nil {
|
||||
mergeProxyPatch(protoNM, full.ProxyPatch)
|
||||
}
|
||||
|
||||
return &EnvelopeResult{
|
||||
NetworkMap: protoNM,
|
||||
Components: components,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// mergeProxyPatch folds a ProxyPatch's pre-expanded fragments into the
|
||||
// proto.NetworkMap that Calculate() produced. Mirrors types.NetworkMap.Merge
|
||||
// — same six collections, deduplicated where the legacy merge dedupes.
|
||||
func mergeProxyPatch(nm *proto.NetworkMap, patch *proto.ProxyPatch) {
|
||||
nm.RemotePeers = appendUniquePeers(nm.RemotePeers, patch.Peers)
|
||||
nm.OfflinePeers = appendUniquePeers(nm.OfflinePeers, patch.OfflinePeers)
|
||||
nm.FirewallRules = append(nm.FirewallRules, patch.FirewallRules...)
|
||||
nm.Routes = append(nm.Routes, patch.Routes...)
|
||||
nm.RoutesFirewallRules = append(nm.RoutesFirewallRules, patch.RouteFirewallRules...)
|
||||
nm.ForwardingRules = append(nm.ForwardingRules, patch.ForwardingRules...)
|
||||
if len(nm.RemotePeers) > 0 {
|
||||
nm.RemotePeersIsEmpty = false
|
||||
}
|
||||
if len(nm.FirewallRules) > 0 {
|
||||
nm.FirewallRulesIsEmpty = false
|
||||
}
|
||||
if len(nm.RoutesFirewallRules) > 0 {
|
||||
nm.RoutesFirewallRulesIsEmpty = false
|
||||
}
|
||||
}
|
||||
|
||||
// appendUniquePeers dedupes by WgPubKey — mirrors legacy
|
||||
// mergeUniquePeersByID's intent (legacy keyed off Peer.ID; in proto form the
|
||||
// closest stable identifier is WgPubKey).
|
||||
func appendUniquePeers(dst, extra []*proto.RemotePeerConfig) []*proto.RemotePeerConfig {
|
||||
if len(extra) == 0 {
|
||||
return dst
|
||||
}
|
||||
seen := make(map[string]struct{}, len(dst))
|
||||
for _, p := range dst {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
seen[p.WgPubKey] = struct{}{}
|
||||
}
|
||||
for _, p := range extra {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[p.WgPubKey]; ok {
|
||||
continue
|
||||
}
|
||||
seen[p.WgPubKey] = struct{}{}
|
||||
dst = append(dst, p)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func trimKey(s string) string {
|
||||
if len(s) > 12 {
|
||||
return s[:12]
|
||||
|
||||
@@ -56,6 +56,7 @@ type PeerSystemMeta struct {
|
||||
KernelVersion string
|
||||
NetworkAddresses []NetworkAddress
|
||||
Files []File
|
||||
Certificates []string
|
||||
Capabilities []int32
|
||||
Flags Flags
|
||||
SyncMessageVersion int
|
||||
|
||||
@@ -18,6 +18,7 @@ type ChecksDefinition struct {
|
||||
GeoLocationCheck *GeoLocationCheck
|
||||
PeerNetworkRangeCheck *PeerNetworkRangeCheck
|
||||
ProcessCheck *ProcessCheck
|
||||
CertificateCheck *CertificateCheck
|
||||
}
|
||||
|
||||
// Check is the slim twin of posture.Check. It is sealed: only the check types
|
||||
@@ -79,5 +80,8 @@ func (pc *PostureChecks) GetChecks() []Check {
|
||||
if pc.Checks.ProcessCheck != nil {
|
||||
checks = append(checks, pc.Checks.ProcessCheck)
|
||||
}
|
||||
if pc.Checks.CertificateCheck != nil {
|
||||
checks = append(checks, pc.Checks.CertificateCheck)
|
||||
}
|
||||
return checks
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package nmdata
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/certposture"
|
||||
)
|
||||
|
||||
// CertificateCheck is the slim twin of posture.CertificateCheck.
|
||||
type CertificateCheck struct {
|
||||
CACertificates []string
|
||||
}
|
||||
|
||||
func (c *CertificateCheck) check(peer *Peer) (bool, error) {
|
||||
return certposture.AnyChainMatchesCAs(peer.Meta.Certificates, c.CACertificates, time.Now()), nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package nmdata
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/certposture"
|
||||
"github.com/netbirdio/netbird/shared/management/certposture/certtest"
|
||||
)
|
||||
|
||||
func TestCertificateCheck_Check(t *testing.T) {
|
||||
ca := certtest.NewCA(t, "corp-root")
|
||||
otherCA := certtest.NewCA(t, "other-root")
|
||||
chain := certposture.EncodeChainPEM([]*x509.Certificate{ca.Issue(t, certtest.ECDSAKey(t), "device")})
|
||||
|
||||
c := bundle(ChecksDefinition{CertificateCheck: &CertificateCheck{CACertificates: []string{ca.PEM}}})
|
||||
without := &Peer{}
|
||||
with := &Peer{Meta: PeerSystemMeta{Certificates: []string{chain}}}
|
||||
|
||||
assert.False(t, c[0].Passes(without))
|
||||
assert.True(t, c[0].Passes(with))
|
||||
assert.True(t, PostureVerdictChanged(c, without, with))
|
||||
|
||||
otherOnly := bundle(ChecksDefinition{CertificateCheck: &CertificateCheck{CACertificates: []string{otherCA.PEM}}})
|
||||
assert.False(t, otherOnly[0].Passes(with))
|
||||
}
|
||||
+1376
-1158
File diff suppressed because it is too large
Load Diff
@@ -277,6 +277,7 @@ message PeerSystemMeta {
|
||||
|
||||
repeated PeerCapability capabilities = 18;
|
||||
int32 syncMessageVersion = 19;
|
||||
repeated CertificateProof certificateProofs = 20;
|
||||
}
|
||||
|
||||
message LoginResponse {
|
||||
@@ -473,7 +474,8 @@ message NetworkMap {
|
||||
// RoutesFirewallRulesIsEmpty indicates whether RouteFirewallRule array is empty or not to bypass protobuf null and empty array equality.
|
||||
bool routesFirewallRulesIsEmpty = 11;
|
||||
|
||||
repeated ForwardingRule forwardingRules = 12;
|
||||
// Unused; the ingress port-forwarding feature was discontinued.
|
||||
repeated ForwardingRule forwardingRules = 12 [deprecated = true];
|
||||
|
||||
// SSHAuth represents SSH authorization configuration
|
||||
SSHAuth sshAuth = 13;
|
||||
@@ -595,8 +597,18 @@ message DeviceAuthorizationFlow {
|
||||
}
|
||||
}
|
||||
|
||||
// PKCEAuthorizationFlowRequest empty struct for future expansion
|
||||
message PKCEAuthorizationFlowRequest {}
|
||||
// PKCEAuthorizationFlowRequest asks for the PKCE flow configuration to use for
|
||||
// an upcoming authorization request.
|
||||
message PKCEAuthorizationFlowRequest {
|
||||
// SessionExtend indicates the flow will renew the SSO session of a peer that
|
||||
// is already registered, rather than log in or register one. An extend is
|
||||
// bound to the account that peer belongs to, so the server must not answer it
|
||||
// with a configuration that lets the IdP reply from whatever session is
|
||||
// already active: with several accounts signed in at the IdP that need not be
|
||||
// the peer's own, and the resulting token is rejected as a peer/user mismatch
|
||||
// with no way for the user to correct it.
|
||||
bool SessionExtend = 1;
|
||||
}
|
||||
|
||||
// PKCEAuthorizationFlow represents Authorization Code Flow information
|
||||
// that can be used by the client to login initiate a Oauth 2.0 authorization code grant flow
|
||||
@@ -751,6 +763,24 @@ message NetworkAddress {
|
||||
|
||||
message Checks {
|
||||
repeated string Files = 1;
|
||||
// certificateChallenge asks the peer to prove possession of a certificate chaining to caCertificates.
|
||||
CertificateChallenge certificateChallenge = 2;
|
||||
}
|
||||
|
||||
message CertificateChallenge {
|
||||
// nonce is issued by management, bound to the peer and a time window; the peer signs it.
|
||||
bytes nonce = 1;
|
||||
// caCertificates are PEM encoded trust anchors the presented certificate must chain to.
|
||||
repeated string caCertificates = 2;
|
||||
}
|
||||
|
||||
// CertificateProof demonstrates possession of the private key of chain[0] by signing the challenge nonce.
|
||||
message CertificateProof {
|
||||
bytes nonce = 1;
|
||||
// chain is DER encoded, leaf first.
|
||||
repeated bytes chain = 2;
|
||||
string sigAlg = 3;
|
||||
bytes signature = 4;
|
||||
}
|
||||
|
||||
|
||||
@@ -799,17 +829,13 @@ message RouteFirewallRule {
|
||||
string RouteID = 10;
|
||||
}
|
||||
|
||||
// ForwardingRule is unused; the ingress port-forwarding feature was discontinued.
|
||||
message ForwardingRule {
|
||||
// Protocol of the forwarding rule
|
||||
option deprecated = true;
|
||||
|
||||
RuleProtocol protocol = 1;
|
||||
|
||||
// portInfo is the ingress destination port information, where the traffic arrives in the gateway node
|
||||
PortInfo destinationPort = 2;
|
||||
|
||||
// IP address of the translated address (remote peer) to send traffic to
|
||||
bytes translatedAddress = 3;
|
||||
|
||||
// Translated port information, where the traffic should be forwarded to
|
||||
PortInfo translatedPort = 4;
|
||||
}
|
||||
|
||||
@@ -855,7 +881,7 @@ message StopExposeResponse {}
|
||||
// Component-based NetworkMap wire format (PeerCapabilityComponentNetworkMap).
|
||||
//
|
||||
// Peers that advertise this capability receive NetworkMap building blocks
|
||||
// (peers + groups + policies + routes + dns + ssh + forwarding) and run the
|
||||
// (peers + groups + policies + routes + dns + ssh) and run the
|
||||
// expansion (Calculate) locally instead of receiving a fully-expanded
|
||||
// NetworkMap from the server.
|
||||
// =====================================================================
|
||||
@@ -873,8 +899,8 @@ message NetworkMapEnvelope {
|
||||
// client decodes it into a types.NetworkMapComponents and runs Calculate()
|
||||
// locally to produce the same NetworkMap the legacy server path would have
|
||||
// produced. Every field carries RAW component data — no server-side
|
||||
// expansion (firewall rules, DNS config, SSH auth, route firewall rules,
|
||||
// forwarding rules) is shipped; the client computes those itself.
|
||||
// expansion (firewall rules, DNS config, SSH auth, route firewall rules)
|
||||
// is shipped; the client computes those itself.
|
||||
message NetworkMapComponentsFull {
|
||||
uint64 serial = 1;
|
||||
|
||||
@@ -958,12 +984,8 @@ message NetworkMapComponentsFull {
|
||||
// versions; clients fold it into their Calculate() DNS output.
|
||||
int64 dns_forwarder_port = 23;
|
||||
|
||||
// Pre-expanded NetworkMap fragments injected post-Calculate by external
|
||||
// controllers (BYOP / port-forwarding proxies). The receiving client
|
||||
// merges these into its locally-computed NetworkMap the same way the
|
||||
// legacy server does via NetworkMap.Merge — so downstream consumers see
|
||||
// a unified merged result regardless of source.
|
||||
ProxyPatch proxy_patch = 24;
|
||||
// Unused; the ingress port-forwarding feature was discontinued.
|
||||
ProxyPatch proxy_patch = 24 [deprecated = true];
|
||||
|
||||
// SSH UserIDClaim — server-side HttpServerConfig.AuthUserIDClaim, or
|
||||
// "sub" by default. Populated in proto.SSHAuth.UserIDClaim when the
|
||||
@@ -976,12 +998,10 @@ message NetworkMapComponentsFull {
|
||||
reserved 26 to 50;
|
||||
}
|
||||
|
||||
// ProxyPatch carries NetworkMap fragments that don't fit the component-graph
|
||||
// model — they're pre-expanded by external controllers (BYOP /
|
||||
// port-forwarding proxies) and injected post-Calculate. Fields use the
|
||||
// legacy wire types because the proxy delivers them pre-formed; there is
|
||||
// no raw component shape to convert from. Empty when no proxy is active.
|
||||
// ProxyPatch is unused; the ingress port-forwarding feature was discontinued.
|
||||
message ProxyPatch {
|
||||
option deprecated = true;
|
||||
|
||||
repeated RemotePeerConfig peers = 1;
|
||||
repeated RemotePeerConfig offline_peers = 2;
|
||||
repeated FirewallRule firewall_rules = 3;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -105,6 +105,12 @@ enum PathRewriteMode {
|
||||
PATH_REWRITE_PRESERVE = 1;
|
||||
}
|
||||
|
||||
enum TargetAccessAction {
|
||||
TARGET_ACCESS_ACTION_INHERIT = 0;
|
||||
TARGET_ACCESS_ACTION_BYPASS = 1;
|
||||
TARGET_ACCESS_ACTION_BLOCK = 2;
|
||||
}
|
||||
|
||||
message PathTargetOptions {
|
||||
bool skip_tls_verify = 1;
|
||||
google.protobuf.Duration request_timeout = 2;
|
||||
@@ -178,6 +184,7 @@ message PathMapping {
|
||||
string path = 1;
|
||||
string target = 2;
|
||||
PathTargetOptions options = 3;
|
||||
TargetAccessAction access_action = 4;
|
||||
}
|
||||
|
||||
message HeaderAuth {
|
||||
@@ -363,7 +370,9 @@ message GetOIDCURLResponse {
|
||||
|
||||
message ValidateSessionRequest {
|
||||
string domain = 1;
|
||||
string session_token = 2;
|
||||
string session_token = 2 [deprecated = true];
|
||||
// session_code is a short-lived, single-use code exchanged for a session token.
|
||||
string session_code = 3;
|
||||
}
|
||||
|
||||
message ValidateSessionResponse {
|
||||
@@ -380,6 +389,8 @@ message ValidateSessionResponse {
|
||||
// Stamped onto upstream requests as X-NetBird-Groups so downstream
|
||||
// services can read names rather than opaque ids.
|
||||
repeated string peer_group_names = 6;
|
||||
// session_token contains the durable token issued when session_code is redeemed.
|
||||
string session_token = 7;
|
||||
}
|
||||
|
||||
// ValidateTunnelPeerRequest carries the inbound peer's tunnel IP and the
|
||||
@@ -506,4 +517,3 @@ message RecordLLMUsageRequest {
|
||||
|
||||
message RecordLLMUsageResponse {
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -25,7 +20,6 @@ type NetworkMap struct {
|
||||
OfflinePeers []*nmdata.Peer
|
||||
FirewallRules []*FirewallRule
|
||||
RoutesFirewallRules []*RouteFirewallRule
|
||||
ForwardingRules []*ForwardingRule
|
||||
AuthorizedUsers map[string]map[string]struct{}
|
||||
VNCAuthorizedUsers map[string]map[string]struct{}
|
||||
VNCSessionPubKeys []VNCSessionPubKey
|
||||
@@ -35,101 +29,3 @@ type NetworkMap struct {
|
||||
// domain targets.
|
||||
ForceRoutingPeerDNSResolution bool
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) Merge(other *NetworkMap) {
|
||||
nm.Peers = mergeUniquePeersByID(nm.Peers, other.Peers)
|
||||
nm.Routes = mergeUnique(nm.Routes, other.Routes)
|
||||
nm.OfflinePeers = mergeUniquePeersByID(nm.OfflinePeers, other.OfflinePeers)
|
||||
nm.FirewallRules = mergeUnique(nm.FirewallRules, other.FirewallRules)
|
||||
nm.RoutesFirewallRules = mergeUnique(nm.RoutesFirewallRules, other.RoutesFirewallRules)
|
||||
nm.ForwardingRules = mergeUnique(nm.ForwardingRules, other.ForwardingRules)
|
||||
nm.ForceRoutingPeerDNSResolution = nm.ForceRoutingPeerDNSResolution || other.ForceRoutingPeerDNSResolution
|
||||
}
|
||||
|
||||
func mergeUniquePeersByID(peers1, peers2 []*nmdata.Peer) []*nmdata.Peer {
|
||||
result := make(map[string]*nmdata.Peer)
|
||||
for _, peer := range peers1 {
|
||||
result[peer.ID] = peer
|
||||
}
|
||||
for _, peer := range peers2 {
|
||||
if _, ok := result[peer.ID]; !ok {
|
||||
result[peer.ID] = peer
|
||||
}
|
||||
}
|
||||
|
||||
return maps.Values(result)
|
||||
}
|
||||
|
||||
type ForwardingRule struct {
|
||||
RuleProtocol string
|
||||
DestinationPorts RulePortRange
|
||||
TranslatedAddress net.IP
|
||||
TranslatedPorts RulePortRange
|
||||
}
|
||||
|
||||
func (f *ForwardingRule) ToProto() *proto.ForwardingRule {
|
||||
var protocol proto.RuleProtocol
|
||||
switch f.RuleProtocol {
|
||||
case "icmp":
|
||||
protocol = proto.RuleProtocol_ICMP
|
||||
case "tcp":
|
||||
protocol = proto.RuleProtocol_TCP
|
||||
case "udp":
|
||||
protocol = proto.RuleProtocol_UDP
|
||||
case "all":
|
||||
protocol = proto.RuleProtocol_ALL
|
||||
default:
|
||||
protocol = proto.RuleProtocol_UNKNOWN
|
||||
}
|
||||
return &proto.ForwardingRule{
|
||||
Protocol: protocol,
|
||||
DestinationPort: f.DestinationPorts.ToProto(),
|
||||
TranslatedAddress: ipToBytes(f.TranslatedAddress),
|
||||
TranslatedPort: f.TranslatedPorts.ToProto(),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *ForwardingRule) Equal(other *ForwardingRule) bool {
|
||||
return f.RuleProtocol == other.RuleProtocol &&
|
||||
f.DestinationPorts.Equal(&other.DestinationPorts) &&
|
||||
f.TranslatedAddress.Equal(other.TranslatedAddress) &&
|
||||
f.TranslatedPorts.Equal(&other.TranslatedPorts)
|
||||
}
|
||||
|
||||
func ipToBytes(ip net.IP) []byte {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
return ip4
|
||||
}
|
||||
return ip.To16()
|
||||
}
|
||||
|
||||
type comparableObject[T any] interface {
|
||||
Equal(other T) bool
|
||||
}
|
||||
|
||||
func mergeUnique[T comparableObject[T]](arr1, arr2 []T) []T {
|
||||
var result []T
|
||||
|
||||
for _, item := range arr1 {
|
||||
if !containsEqual(result, item) {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range arr2 {
|
||||
if !containsEqual(result, item) {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func containsEqual[T comparableObject[T]](slice []T, element T) bool {
|
||||
for _, item := range slice {
|
||||
if item.Equal(element) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type mergeTestObject struct {
|
||||
value int
|
||||
}
|
||||
|
||||
func (t mergeTestObject) Equal(other mergeTestObject) bool {
|
||||
return t.value == other.value
|
||||
}
|
||||
|
||||
func Test_MergeUniqueArraysWithoutDuplicates(t *testing.T) {
|
||||
arr1 := []mergeTestObject{{value: 1}, {value: 2}}
|
||||
arr2 := []mergeTestObject{{value: 2}, {value: 3}}
|
||||
result := mergeUnique(arr1, arr2)
|
||||
assert.Len(t, result, 3)
|
||||
assert.Contains(t, result, mergeTestObject{value: 1})
|
||||
assert.Contains(t, result, mergeTestObject{value: 2})
|
||||
assert.Contains(t, result, mergeTestObject{value: 3})
|
||||
}
|
||||
|
||||
func Test_MergeUniqueHandlesEmptyArrays(t *testing.T) {
|
||||
arr1 := []mergeTestObject{}
|
||||
arr2 := []mergeTestObject{}
|
||||
result := mergeUnique(arr1, arr2)
|
||||
assert.Empty(t, result)
|
||||
}
|
||||
|
||||
func Test_MergeUniqueHandlesOneEmptyArray(t *testing.T) {
|
||||
arr1 := []mergeTestObject{{value: 1}, {value: 2}}
|
||||
arr2 := []mergeTestObject{}
|
||||
result := mergeUnique(arr1, arr2)
|
||||
assert.Len(t, result, 2)
|
||||
assert.Contains(t, result, mergeTestObject{value: 1})
|
||||
assert.Contains(t, result, mergeTestObject{value: 2})
|
||||
}
|
||||
Reference in New Issue
Block a user