Merge branch 'main' into feature/extend-rest-client

This commit is contained in:
Pedro Costa
2025-05-12 15:26:07 +01:00
165 changed files with 7450 additions and 2384 deletions
+4 -4
View File
@@ -159,6 +159,7 @@ func (c *GrpcClient) handleStream(ctx context.Context, serverPubKey wgtypes.Key,
// blocking until error
err = c.receiveEvents(stream, serverPubKey, msgHandler)
if err != nil {
c.notifyDisconnected(err)
s, _ := gstatus.FromError(err)
switch s.Code() {
case codes.PermissionDenied:
@@ -167,7 +168,6 @@ func (c *GrpcClient) handleStream(ctx context.Context, serverPubKey wgtypes.Key,
log.Debugf("management connection context has been canceled, this usually indicates shutdown")
return nil
default:
c.notifyDisconnected(err)
log.Warnf("disconnected from the Management service but will retry silently. Reason: %v", err)
return err
}
@@ -258,10 +258,10 @@ func (c *GrpcClient) receiveEvents(stream proto.ManagementService_SyncClient, se
return err
}
err = msgHandler(decryptedResp)
if err != nil {
if err := msgHandler(decryptedResp); err != nil {
log.Errorf("failed handling an update message received from Management Service: %v", err.Error())
return err
// hide any grpc error code that is not relevant for management
return fmt.Errorf("msg handler error: %v", err.Error())
}
}
}
+9 -3
View File
@@ -20,7 +20,9 @@ func (a *AccountsAPI) List(ctx context.Context) ([]api.Account, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.Account](resp)
return ret, err
}
@@ -36,7 +38,9 @@ func (a *AccountsAPI) Update(ctx context.Context, accountID string, request api.
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Account](resp)
return &ret, err
}
@@ -48,7 +52,9 @@ func (a *AccountsAPI) Delete(ctx context.Context, accountID string) error {
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
+21 -7
View File
@@ -20,7 +20,9 @@ func (a *DNSAPI) ListNameserverGroups(ctx context.Context) ([]api.NameserverGrou
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.NameserverGroup](resp)
return ret, err
}
@@ -32,7 +34,9 @@ func (a *DNSAPI) GetNameserverGroup(ctx context.Context, nameserverGroupID strin
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.NameserverGroup](resp)
return &ret, err
}
@@ -48,7 +52,9 @@ func (a *DNSAPI) CreateNameserverGroup(ctx context.Context, request api.PostApiD
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.NameserverGroup](resp)
return &ret, err
}
@@ -64,7 +70,9 @@ func (a *DNSAPI) UpdateNameserverGroup(ctx context.Context, nameserverGroupID st
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.NameserverGroup](resp)
return &ret, err
}
@@ -76,7 +84,9 @@ func (a *DNSAPI) DeleteNameserverGroup(ctx context.Context, nameserverGroupID st
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
@@ -88,7 +98,9 @@ func (a *DNSAPI) GetSettings(ctx context.Context) (*api.DNSSettings, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.DNSSettings](resp)
return &ret, err
}
@@ -104,7 +116,9 @@ func (a *DNSAPI) UpdateSettings(ctx context.Context, request api.PutApiDnsSettin
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.DNSSettings](resp)
return &ret, err
}
+3 -1
View File
@@ -18,7 +18,9 @@ func (a *EventsAPI) List(ctx context.Context) ([]api.Event, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.Event](resp)
return ret, err
}
+6 -2
View File
@@ -18,7 +18,9 @@ func (a *GeoLocationAPI) ListCountries(ctx context.Context) ([]api.Country, erro
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.Country](resp)
return ret, err
}
@@ -30,7 +32,9 @@ func (a *GeoLocationAPI) ListCountryCities(ctx context.Context, countryCode stri
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.City](resp)
return ret, err
}
+15 -5
View File
@@ -20,7 +20,9 @@ func (a *GroupsAPI) List(ctx context.Context) ([]api.Group, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.Group](resp)
return ret, err
}
@@ -32,7 +34,9 @@ func (a *GroupsAPI) Get(ctx context.Context, groupID string) (*api.Group, error)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Group](resp)
return &ret, err
}
@@ -48,7 +52,9 @@ func (a *GroupsAPI) Create(ctx context.Context, request api.PostApiGroupsJSONReq
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Group](resp)
return &ret, err
}
@@ -64,7 +70,9 @@ func (a *GroupsAPI) Update(ctx context.Context, groupID string, request api.PutA
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Group](resp)
return &ret, err
}
@@ -76,7 +84,9 @@ func (a *GroupsAPI) Delete(ctx context.Context, groupID string) error {
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
+45 -15
View File
@@ -20,7 +20,9 @@ func (a *NetworksAPI) List(ctx context.Context) ([]api.Network, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.Network](resp)
return ret, err
}
@@ -32,7 +34,9 @@ func (a *NetworksAPI) Get(ctx context.Context, networkID string) (*api.Network,
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Network](resp)
return &ret, err
}
@@ -48,7 +52,9 @@ func (a *NetworksAPI) Create(ctx context.Context, request api.PostApiNetworksJSO
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Network](resp)
return &ret, err
}
@@ -64,7 +70,9 @@ func (a *NetworksAPI) Update(ctx context.Context, networkID string, request api.
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Network](resp)
return &ret, err
}
@@ -76,7 +84,9 @@ func (a *NetworksAPI) Delete(ctx context.Context, networkID string) error {
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
@@ -102,7 +112,9 @@ func (a *NetworkResourcesAPI) List(ctx context.Context) ([]api.NetworkResource,
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.NetworkResource](resp)
return ret, err
}
@@ -114,7 +126,9 @@ func (a *NetworkResourcesAPI) Get(ctx context.Context, networkResourceID string)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.NetworkResource](resp)
return &ret, err
}
@@ -130,7 +144,9 @@ func (a *NetworkResourcesAPI) Create(ctx context.Context, request api.PostApiNet
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.NetworkResource](resp)
return &ret, err
}
@@ -146,7 +162,9 @@ func (a *NetworkResourcesAPI) Update(ctx context.Context, networkResourceID stri
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.NetworkResource](resp)
return &ret, err
}
@@ -158,7 +176,9 @@ func (a *NetworkResourcesAPI) Delete(ctx context.Context, networkResourceID stri
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
@@ -184,7 +204,9 @@ func (a *NetworkRoutersAPI) List(ctx context.Context) ([]api.NetworkRouter, erro
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.NetworkRouter](resp)
return ret, err
}
@@ -196,7 +218,9 @@ func (a *NetworkRoutersAPI) Get(ctx context.Context, networkRouterID string) (*a
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.NetworkRouter](resp)
return &ret, err
}
@@ -212,7 +236,9 @@ func (a *NetworkRoutersAPI) Create(ctx context.Context, request api.PostApiNetwo
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.NetworkRouter](resp)
return &ret, err
}
@@ -228,7 +254,9 @@ func (a *NetworkRoutersAPI) Update(ctx context.Context, networkRouterID string,
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.NetworkRouter](resp)
return &ret, err
}
@@ -240,7 +268,9 @@ func (a *NetworkRoutersAPI) Delete(ctx context.Context, networkRouterID string)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
+15 -5
View File
@@ -20,7 +20,9 @@ func (a *PeersAPI) List(ctx context.Context) ([]api.Peer, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.Peer](resp)
return ret, err
}
@@ -32,7 +34,9 @@ func (a *PeersAPI) Get(ctx context.Context, peerID string) (*api.Peer, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Peer](resp)
return &ret, err
}
@@ -48,7 +52,9 @@ func (a *PeersAPI) Update(ctx context.Context, peerID string, request api.PutApi
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Peer](resp)
return &ret, err
}
@@ -60,7 +66,9 @@ func (a *PeersAPI) Delete(ctx context.Context, peerID string) error {
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
@@ -72,7 +80,9 @@ func (a *PeersAPI) ListAccessiblePeers(ctx context.Context, peerID string) ([]ap
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.Peer](resp)
return ret, err
}
+15 -5
View File
@@ -24,7 +24,9 @@ func (a *PoliciesAPI) List(ctx context.Context, accountID string) ([]api.Policy,
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.Policy](resp)
return ret, err
}
@@ -36,7 +38,9 @@ func (a *PoliciesAPI) Get(ctx context.Context, policyID string) (*api.Policy, er
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Policy](resp)
return &ret, err
}
@@ -52,7 +56,9 @@ func (a *PoliciesAPI) Create(ctx context.Context, request api.PostApiPoliciesJSO
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Policy](resp)
return &ret, err
}
@@ -72,7 +78,9 @@ func (a *PoliciesAPI) Update(ctx context.Context, policyID string, request api.P
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Policy](resp)
return &ret, err
}
@@ -84,7 +92,9 @@ func (a *PoliciesAPI) Delete(ctx context.Context, policyID string) error {
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
+15 -5
View File
@@ -20,7 +20,9 @@ func (a *PostureChecksAPI) List(ctx context.Context) ([]api.PostureCheck, error)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.PostureCheck](resp)
return ret, err
}
@@ -32,7 +34,9 @@ func (a *PostureChecksAPI) Get(ctx context.Context, postureCheckID string) (*api
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.PostureCheck](resp)
return &ret, err
}
@@ -48,7 +52,9 @@ func (a *PostureChecksAPI) Create(ctx context.Context, request api.PostApiPostur
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.PostureCheck](resp)
return &ret, err
}
@@ -64,7 +70,9 @@ func (a *PostureChecksAPI) Update(ctx context.Context, postureCheckID string, re
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.PostureCheck](resp)
return &ret, err
}
@@ -76,7 +84,9 @@ func (a *PostureChecksAPI) Delete(ctx context.Context, postureCheckID string) er
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
+15 -5
View File
@@ -20,7 +20,9 @@ func (a *RoutesAPI) List(ctx context.Context) ([]api.Route, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.Route](resp)
return ret, err
}
@@ -32,7 +34,9 @@ func (a *RoutesAPI) Get(ctx context.Context, routeID string) (*api.Route, error)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Route](resp)
return &ret, err
}
@@ -48,7 +52,9 @@ func (a *RoutesAPI) Create(ctx context.Context, request api.PostApiRoutesJSONReq
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Route](resp)
return &ret, err
}
@@ -64,7 +70,9 @@ func (a *RoutesAPI) Update(ctx context.Context, routeID string, request api.PutA
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Route](resp)
return &ret, err
}
@@ -76,7 +84,9 @@ func (a *RoutesAPI) Delete(ctx context.Context, routeID string) error {
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
+15 -5
View File
@@ -20,7 +20,9 @@ func (a *SetupKeysAPI) List(ctx context.Context) ([]api.SetupKey, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.SetupKey](resp)
return ret, err
}
@@ -32,7 +34,9 @@ func (a *SetupKeysAPI) Get(ctx context.Context, setupKeyID string) (*api.SetupKe
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.SetupKey](resp)
return &ret, err
}
@@ -53,7 +57,9 @@ func (a *SetupKeysAPI) Create(ctx context.Context, request api.PostApiSetupKeysJ
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.SetupKeyClear](resp)
return &ret, err
}
@@ -69,7 +75,9 @@ func (a *SetupKeysAPI) Update(ctx context.Context, setupKeyID string, request ap
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.SetupKey](resp)
return &ret, err
}
@@ -81,7 +89,9 @@ func (a *SetupKeysAPI) Delete(ctx context.Context, setupKeyID string) error {
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
+12 -4
View File
@@ -20,7 +20,9 @@ func (a *TokensAPI) List(ctx context.Context, userID string) ([]api.PersonalAcce
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.PersonalAccessToken](resp)
return ret, err
}
@@ -32,7 +34,9 @@ func (a *TokensAPI) Get(ctx context.Context, userID, tokenID string) (*api.Perso
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.PersonalAccessToken](resp)
return &ret, err
}
@@ -48,7 +52,9 @@ func (a *TokensAPI) Create(ctx context.Context, userID string, request api.PostA
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.PersonalAccessTokenGenerated](resp)
return &ret, err
}
@@ -60,7 +66,9 @@ func (a *TokensAPI) Delete(ctx context.Context, userID, tokenID string) error {
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
+18 -6
View File
@@ -20,7 +20,9 @@ func (a *UsersAPI) List(ctx context.Context) ([]api.User, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.User](resp)
return ret, err
}
@@ -36,7 +38,9 @@ func (a *UsersAPI) Create(ctx context.Context, request api.PostApiUsersJSONReque
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.User](resp)
return &ret, err
}
@@ -52,7 +56,9 @@ func (a *UsersAPI) Update(ctx context.Context, userID string, request api.PutApi
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.User](resp)
return &ret, err
}
@@ -64,7 +70,9 @@ func (a *UsersAPI) Delete(ctx context.Context, userID string) error {
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
@@ -76,7 +84,9 @@ func (a *UsersAPI) ResendInvitation(ctx context.Context, userID string) error {
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
@@ -88,7 +98,9 @@ func (a *UsersAPI) Current(ctx context.Context) (*api.User, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.User](resp)
return &ret, err
+2 -5
View File
@@ -30,11 +30,8 @@ var (
Issued: ptr("api"),
LastLogin: &time.Time{},
Name: "M. Essam",
Permissions: &api.UserPermissions{
DashboardView: ptr(api.UserPermissionsDashboardViewFull),
},
Role: "user",
Status: api.UserStatusActive,
Role: "user",
Status: api.UserStatusActive,
}
)
+9 -3
View File
@@ -1,12 +1,17 @@
package domain
import (
"strings"
"golang.org/x/net/idna"
)
// Domain represents a punycode-encoded domain string.
// This should only be converted from a string when the string already is in punycode, otherwise use FromString.
type Domain string
// String converts the Domain to a non-punycode string.
// For an infallible conversion, use SafeString.
func (d Domain) String() (string, error) {
unicode, err := idna.ToUnicode(string(d))
if err != nil {
@@ -15,16 +20,17 @@ func (d Domain) String() (string, error) {
return unicode, nil
}
// SafeString converts the Domain to a non-punycode string, falling back to the original string if conversion fails.
// SafeString converts the Domain to a non-punycode string, falling back to the punycode string if conversion fails.
func (d Domain) SafeString() string {
str, err := d.String()
if err != nil {
str = string(d)
return string(d)
}
return str
}
// PunycodeString returns the punycode representation of the Domain.
// This should only be used if a punycode domain is expected but only a string is supported.
func (d Domain) PunycodeString() string {
return string(d)
}
@@ -35,5 +41,5 @@ func FromString(s string) (Domain, error) {
if err != nil {
return "", err
}
return Domain(ascii), nil
return Domain(strings.ToLower(ascii)), nil
}
+3 -2
View File
@@ -5,6 +5,7 @@ import (
"strings"
)
// List is a slice of punycode-encoded domain strings.
type List []Domain
// ToStringList converts a List to a slice of string.
@@ -53,7 +54,7 @@ func (d List) String() (string, error) {
func (d List) SafeString() string {
str, err := d.String()
if err != nil {
return strings.Join(d.ToPunycodeList(), ", ")
return d.PunycodeString()
}
return str
}
@@ -101,7 +102,7 @@ func FromStringList(s []string) (List, error) {
func FromPunycodeList(s []string) List {
var dl List
for _, domain := range s {
dl = append(dl, Domain(domain))
dl = append(dl, Domain(strings.ToLower(domain)))
}
return dl
}
-2
View File
@@ -22,8 +22,6 @@ func ValidateDomains(domains []string) (List, error) {
var domainList List
for _, d := range domains {
d := strings.ToLower(d)
// handles length and idna conversion
punycode, err := FromString(d)
if err != nil {
+65 -54
View File
@@ -3057,6 +3057,8 @@ type RouteFirewallRule struct {
CustomProtocol uint32 `protobuf:"varint,8,opt,name=customProtocol,proto3" json:"customProtocol,omitempty"`
// PolicyID is the ID of the policy that this rule belongs to
PolicyID []byte `protobuf:"bytes,9,opt,name=PolicyID,proto3" json:"PolicyID,omitempty"`
// RouteID is the ID of the route that this rule belongs to
RouteID string `protobuf:"bytes,10,opt,name=RouteID,proto3" json:"RouteID,omitempty"`
}
func (x *RouteFirewallRule) Reset() {
@@ -3154,6 +3156,13 @@ func (x *RouteFirewallRule) GetPolicyID() []byte {
return nil
}
func (x *RouteFirewallRule) GetRouteID() string {
if x != nil {
return x.RouteID
}
return ""
}
type ForwardingRule struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -3702,7 +3711,7 @@ var file_management_proto_rawDesc = []byte{
0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52,
0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20,
0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74,
0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xed, 0x02, 0x0a, 0x11, 0x52, 0x6f,
0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x87, 0x03, 0x0a, 0x11, 0x52, 0x6f,
0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12,
0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18,
0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e,
@@ -3725,66 +3734,68 @@ var file_management_proto_rawDesc = []byte{
0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63,
0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1a, 0x0a,
0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x22, 0xf2, 0x01, 0x0a, 0x0e, 0x46, 0x6f,
0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x08,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18,
0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65,
0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66,
0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f,
0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64,
0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x74,
0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f,
0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e,
0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x2a, 0x4c,
0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b,
0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41,
0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a,
0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04,
0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d,
0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a,
0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22,
0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06,
0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50,
0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69,
0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75,
0x74, 0x65, 0x49, 0x44, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74,
0x65, 0x49, 0x44, 0x22, 0xf2, 0x01, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69,
0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63,
0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f,
0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18,
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73,
0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11,
0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73,
0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61,
0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72,
0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c,
0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65,
0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e,
0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07,
0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03,
0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55,
0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69,
0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12,
0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65,
0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54,
0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a,
0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e,
0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a,
0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63,
0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12,
0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73,
0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65,
0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e,
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65,
0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69,
0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00,
0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74,
0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c,
0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72,
0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70,
0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18,
0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61,
0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30,
0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65,
0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74,
0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65,
0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73,
0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65,
0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43,
0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c,
0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e,
0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00,
0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70,
0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e,
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42,
0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
}
var (
+3
View File
@@ -509,6 +509,9 @@ message RouteFirewallRule {
// PolicyID is the ID of the policy that this rule belongs to
bytes PolicyID = 9;
// RouteID is the ID of the route that this rule belongs to
string RouteID = 10;
}
message ForwardingRule {
+88 -9
View File
@@ -17,6 +17,7 @@ import (
"time"
cacheStore "github.com/eko/gocache/lib/v4/store"
"github.com/eko/gocache/store/redis/v4"
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
"github.com/vmihailenco/msgpack/v5"
@@ -237,7 +238,7 @@ func BuildManager(
if !isNil(am.idpManager) {
go func() {
err := am.warmupIDPCache(ctx)
err := am.warmupIDPCache(ctx, cacheStore)
if err != nil {
log.WithContext(ctx).Warnf("failed warming up cache due to error: %v", err)
// todo retry?
@@ -275,6 +276,10 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
return nil, status.Errorf(status.InvalidArgument, "peer login expiration can't be smaller than one hour")
}
if newSettings.DNSDomain != "" && !isDomainValid(newSettings.DNSDomain) {
return nil, status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for DNS domain", newSettings.DNSDomain)
}
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
@@ -325,6 +330,12 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
account.Network.Serial++
}
if oldSettings.DNSDomain != newSettings.DNSDomain {
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountDNSDomainUpdated, nil)
updateAccountPeers = true
account.Network.Serial++
}
err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID)
if err != nil {
return nil, err
@@ -484,7 +495,25 @@ func (am *DefaultAccountManager) newAccount(ctx context.Context, userID, domain
return nil, status.Errorf(status.Internal, "error while creating new account")
}
func (am *DefaultAccountManager) warmupIDPCache(ctx context.Context) error {
func (am *DefaultAccountManager) warmupIDPCache(ctx context.Context, store cacheStore.StoreInterface) error {
cold, err := am.isCacheCold(ctx, store)
if err != nil {
return err
}
if !cold {
log.WithContext(ctx).Debug("cache already populated, skipping warm up")
return nil
}
if delayStr, ok := os.LookupEnv("NB_IDP_CACHE_WARMUP_DELAY"); ok {
delay, err := time.ParseDuration(delayStr)
if err != nil {
return fmt.Errorf("invalid IDP warmup delay: %w", err)
}
time.Sleep(delay)
}
userData, err := am.idpManager.GetAllAccounts(ctx)
if err != nil {
return err
@@ -524,6 +553,32 @@ func (am *DefaultAccountManager) warmupIDPCache(ctx context.Context) error {
return nil
}
// isCacheCold checks if the cache needs warming up.
func (am *DefaultAccountManager) isCacheCold(ctx context.Context, store cacheStore.StoreInterface) (bool, error) {
if store.GetType() != redis.RedisType {
return true, nil
}
accountID, err := am.Store.GetAnyAccountID(ctx)
if err != nil {
if sErr, ok := status.FromError(err); ok && sErr.Type() == status.NotFound {
return true, nil
}
return false, err
}
_, err = store.Get(ctx, accountID)
if err == nil {
return false, nil
}
if notFoundErr := new(cacheStore.NotFound); errors.As(err, &notFoundErr) {
return true, nil
}
return false, fmt.Errorf("failed to check cache: %w", err)
}
// DeleteAccount deletes an account and all its users from local store and from the remote IDP if the requester is an admin and account owner
func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, userID string) error {
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
@@ -548,11 +603,15 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u
}
for _, otherUser := range account.Users {
if otherUser.IsServiceUser {
if otherUser.Id == userID {
continue
}
if otherUser.Id == userID {
if otherUser.IsServiceUser {
err = am.deleteServiceUser(ctx, accountID, userID, otherUser)
if err != nil {
return err
}
continue
}
@@ -657,7 +716,7 @@ func (am *DefaultAccountManager) loadAccount(ctx context.Context, accountID any)
log.WithContext(ctx).Debugf("account %s not found in cache, reloading", accountID)
accountIDString := fmt.Sprintf("%v", accountID)
account, err := am.Store.GetAccount(ctx, accountIDString)
accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountIDString)
if err != nil {
return nil, nil, err
}
@@ -666,7 +725,7 @@ func (am *DefaultAccountManager) loadAccount(ctx context.Context, accountID any)
if err != nil {
return nil, nil, err
}
log.WithContext(ctx).Debugf("%d entries received from IdP management", len(userData))
log.WithContext(ctx).Debugf("%d entries received from IdP management for account %s", len(userData), accountIDString)
dataMap := make(map[string]*idp.UserData, len(userData))
for _, datum := range userData {
@@ -674,7 +733,7 @@ func (am *DefaultAccountManager) loadAccount(ctx context.Context, accountID any)
}
matchedUserData := make([]*idp.UserData, 0)
for _, user := range account.Users {
for _, user := range accountUsers {
if user.IsServiceUser {
continue
}
@@ -1057,6 +1116,19 @@ func (am *DefaultAccountManager) GetAccountByID(ctx context.Context, accountID s
return am.Store.GetAccount(ctx, accountID)
}
// GetAccountMeta returns the account metadata associated with this account ID.
func (am *DefaultAccountManager) GetAccountMeta(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error) {
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Accounts, operations.Read)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
if !allowed {
return nil, status.NewPermissionDeniedError()
}
return am.Store.GetAccountMeta(ctx, store.LockingStrengthShare, accountID)
}
func (am *DefaultAccountManager) GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) {
if userAuth.UserId == "" {
return "", "", errors.New(emptyUserID)
@@ -1480,8 +1552,15 @@ func isDomainValid(domain string) bool {
}
// GetDNSDomain returns the configured dnsDomain
func (am *DefaultAccountManager) GetDNSDomain() string {
return am.dnsDomain
func (am *DefaultAccountManager) GetDNSDomain(settings *types.Settings) string {
if settings == nil {
return am.dnsDomain
}
if settings.DNSDomain == "" {
return am.dnsDomain
}
return settings.DNSDomain
}
func (am *DefaultAccountManager) onPeersInvalidated(ctx context.Context, accountID string) {
+4 -2
View File
@@ -16,6 +16,7 @@ import (
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/users"
"github.com/netbirdio/netbird/route"
)
@@ -37,6 +38,7 @@ type Manager interface {
SaveOrAddUsers(ctx context.Context, accountID, initiatorUserID string, updates []*types.User, addIfNotExists bool) ([]*types.UserInfo, error)
GetSetupKey(ctx context.Context, accountID, userID, keyID string) (*types.SetupKey, error)
GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error)
GetAccountMeta(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error)
AccountExists(ctx context.Context, accountID string) (bool, error)
GetAccountIDByUserID(ctx context.Context, userID, domain string) (string, error)
GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
@@ -80,7 +82,7 @@ type Manager interface {
SaveNameServerGroup(ctx context.Context, accountID, userID string, nsGroupToSave *nbdns.NameServerGroup) error
DeleteNameServerGroup(ctx context.Context, accountID, nsGroupID, userID string) error
ListNameServerGroups(ctx context.Context, accountID string, userID string) ([]*nbdns.NameServerGroup, error)
GetDNSDomain() string
GetDNSDomain(settings *types.Settings) string
StoreEvent(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any)
GetEvents(ctx context.Context, accountID, userID string) ([]*activity.Event, error)
GetDNSSettings(ctx context.Context, accountID string, userID string) (*types.DNSSettings, error)
@@ -114,5 +116,5 @@ type Manager interface {
CreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, error)
UpdateToPrimaryAccount(ctx context.Context, accountId string) (*types.Account, error)
GetOwnerInfo(ctx context.Context, accountId string) (*types.UserInfo, error)
GetCurrentUserInfo(ctx context.Context, accountID, userID string) (*types.UserInfo, error)
GetCurrentUserInfo(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error)
}
+105 -11
View File
@@ -14,30 +14,30 @@ import (
"time"
"github.com/golang/mock/gomock"
nbAccount "github.com/netbirdio/netbird/management/server/account"
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
"github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/settings"
"github.com/netbirdio/netbird/management/server/util"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
"github.com/netbirdio/netbird/management/server/idp"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
nbdns "github.com/netbirdio/netbird/dns"
nbAccount "github.com/netbirdio/netbird/management/server/account"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/cache"
nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/settings"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/telemetry"
"github.com/netbirdio/netbird/management/server/testutil"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/util"
"github.com/netbirdio/netbird/route"
)
@@ -853,6 +853,42 @@ func TestAccountManager_DeleteAccount(t *testing.T) {
t.Fatal(err)
}
account.Users["service-user-1"] = &types.User{
Id: "service-user-1",
Role: types.UserRoleAdmin,
IsServiceUser: true,
Issued: types.UserIssuedAPI,
PATs: map[string]*types.PersonalAccessToken{
"pat-1": {
ID: "pat-1",
UserID: "service-user-1",
Name: "service-user-1",
HashedToken: "hashedToken",
CreatedAt: time.Now(),
},
},
}
account.Users[userId] = &types.User{
Id: "service-user-2",
Role: types.UserRoleUser,
IsServiceUser: true,
Issued: types.UserIssuedAPI,
PATs: map[string]*types.PersonalAccessToken{
"pat-2": {
ID: "pat-2",
UserID: userId,
Name: userId,
HashedToken: "hashedToken",
CreatedAt: time.Now(),
},
},
}
err = manager.Store.SaveAccount(context.Background(), account)
if err != nil {
t.Fatal(err)
}
err = manager.DeleteAccount(context.Background(), account.Id, userId)
if err != nil {
t.Fatal(err)
@@ -862,6 +898,14 @@ func TestAccountManager_DeleteAccount(t *testing.T) {
if err == nil {
t.Fatal(fmt.Errorf("expected to get an error when trying to get deleted account, got %v", getAccount))
}
pats, err := manager.Store.GetUserPATs(context.Background(), store.LockingStrengthShare, "service-user-1")
require.NoError(t, err)
assert.Len(t, pats, 0)
pats, err = manager.Store.GetUserPATs(context.Background(), store.LockingStrengthShare, userId)
require.NoError(t, err)
assert.Len(t, pats, 0)
}
func BenchmarkTest_GetAccountWithclaims(b *testing.B) {
@@ -3201,3 +3245,53 @@ func Test_UpdateToPrimaryAccount(t *testing.T) {
assert.NoError(t, err)
assert.True(t, account.IsDomainPrimaryAccount)
}
func TestDefaultAccountManager_IsCacheCold(t *testing.T) {
manager, err := createManager(t)
require.NoError(t, err)
t.Run("memory cache", func(t *testing.T) {
t.Run("should always return true", func(t *testing.T) {
cacheStore, err := cache.NewStore(context.Background(), 100*time.Millisecond, 300*time.Millisecond)
require.NoError(t, err)
cold, err := manager.isCacheCold(context.Background(), cacheStore)
assert.NoError(t, err)
assert.True(t, cold)
})
})
t.Run("redis cache", func(t *testing.T) {
cleanup, redisURL, err := testutil.CreateRedisTestContainer()
require.NoError(t, err)
t.Cleanup(cleanup)
t.Setenv(cache.RedisStoreEnvVar, redisURL)
cacheStore, err := cache.NewStore(context.Background(), 100*time.Millisecond, 300*time.Millisecond)
require.NoError(t, err)
t.Run("should return true when no account exists", func(t *testing.T) {
cold, err := manager.isCacheCold(context.Background(), cacheStore)
assert.NoError(t, err)
assert.True(t, cold)
})
account, err := manager.GetOrCreateAccountByUser(context.Background(), userID, "")
require.NoError(t, err)
t.Run("should return true when account is not found in cache", func(t *testing.T) {
cold, err := manager.isCacheCold(context.Background(), cacheStore)
assert.NoError(t, err)
assert.True(t, cold)
})
t.Run("should return false when account is found in cache", func(t *testing.T) {
err = cacheStore.Set(context.Background(), account.Id, &idp.UserData{ID: "v", Name: "vv"})
require.NoError(t, err)
cold, err := manager.isCacheCold(context.Background(), cacheStore)
assert.NoError(t, err)
assert.False(t, cold)
})
})
}
+4
View File
@@ -169,6 +169,8 @@ const (
ResourceAddedToGroup Activity = 82
ResourceRemovedFromGroup Activity = 83
AccountDNSDomainUpdated Activity = 84
)
var activityMap = map[Activity]Code{
@@ -264,6 +266,8 @@ var activityMap = map[Activity]Code{
ResourceAddedToGroup: {"Resource added to group", "resource.group.add"},
ResourceRemovedFromGroup: {"Resource removed from group", "resource.group.delete"},
AccountDNSDomainUpdated: {"Account DNS domain updated", "account.dns.domain.update"},
}
// StringCode returns a string code of the activity
+3 -14
View File
@@ -8,12 +8,11 @@ import (
"github.com/eko/gocache/lib/v4/store"
"github.com/redis/go-redis/v9"
"github.com/testcontainers/testcontainers-go"
testcontainersredis "github.com/testcontainers/testcontainers-go/modules/redis"
"github.com/vmihailenco/msgpack/v5"
"github.com/netbirdio/netbird/management/server/cache"
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/testutil"
)
func TestNewIDPCacheManagers(t *testing.T) {
@@ -27,21 +26,11 @@ func TestNewIDPCacheManagers(t *testing.T) {
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
if tc.redis {
ctx := context.Background()
redisContainer, err := testcontainersredis.RunContainer(ctx, testcontainers.WithImage("redis:7"))
cleanup, redisURL, err := testutil.CreateRedisTestContainer()
if err != nil {
t.Fatalf("couldn't start redis container: %s", err)
}
defer func() {
if err := redisContainer.Terminate(ctx); err != nil {
t.Logf("failed to terminate container: %s", err)
}
}()
redisURL, err := redisContainer.ConnectionString(ctx)
if err != nil {
t.Fatalf("couldn't get connection string: %s", err)
}
t.Cleanup(cleanup)
t.Setenv(cache.RedisStoreEnvVar, redisURL)
}
cacheStore, err := cache.NewStore(context.Background(), cache.DefaultIDPCacheExpirationMax, cache.DefaultIDPCacheCleanupInterval)
+9 -2
View File
@@ -158,6 +158,13 @@ func (am *DefaultAccountManager) prepareGroupEvents(ctx context.Context, transac
return nil
}
settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
log.WithContext(ctx).Debugf("failed to get account settings for group events: %v", err)
return nil
}
dnsDomain := am.GetDNSDomain(settings)
for _, peerID := range addedPeers {
peer, ok := peers[peerID]
if !ok {
@@ -168,7 +175,7 @@ func (am *DefaultAccountManager) prepareGroupEvents(ctx context.Context, transac
eventsToStore = append(eventsToStore, func() {
meta := map[string]any{
"group": newGroup.Name, "group_id": newGroup.ID,
"peer_ip": peer.IP.String(), "peer_fqdn": peer.FQDN(am.GetDNSDomain()),
"peer_ip": peer.IP.String(), "peer_fqdn": peer.FQDN(dnsDomain),
}
am.StoreEvent(ctx, userID, peer.ID, accountID, activity.GroupAddedToPeer, meta)
})
@@ -184,7 +191,7 @@ func (am *DefaultAccountManager) prepareGroupEvents(ctx context.Context, transac
eventsToStore = append(eventsToStore, func() {
meta := map[string]any{
"group": newGroup.Name, "group_id": newGroup.ID,
"peer_ip": peer.IP.String(), "peer_fqdn": peer.FQDN(am.GetDNSDomain()),
"peer_ip": peer.IP.String(), "peer_fqdn": peer.FQDN(dnsDomain),
}
am.StoreEvent(ctx, userID, peer.ID, accountID, activity.GroupRemovedFromPeer, meta)
})
+31 -13
View File
@@ -480,20 +480,12 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p
s.ephemeralManager.OnPeerDisconnected(ctx, peer)
}
var relayToken *Token
if s.config.Relay != nil && len(s.config.Relay.Addresses) > 0 {
relayToken, err = s.secretsManager.GenerateRelayToken()
if err != nil {
log.Errorf("failed generating Relay token: %v", err)
}
loginResp, err := s.prepareLoginResponse(ctx, peer, netMap, postureChecks)
if err != nil {
log.WithContext(ctx).Warnf("failed preparing login response for peer %s: %s", peerKey, err)
return nil, status.Errorf(codes.Internal, "failed logging in peer")
}
// if peer has reached this point then it has logged in
loginResp := &proto.LoginResponse{
NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil),
PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain(), false),
Checks: toProtocolChecks(ctx, postureChecks),
}
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, loginResp)
if err != nil {
log.WithContext(ctx).Warnf("failed encrypting peer %s message", peer.ID)
@@ -506,6 +498,32 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p
}, nil
}
func (s *GRPCServer) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, netMap *types.NetworkMap, postureChecks []*posture.Checks) (*proto.LoginResponse, error) {
var relayToken *Token
var err error
if s.config.Relay != nil && len(s.config.Relay.Addresses) > 0 {
relayToken, err = s.secretsManager.GenerateRelayToken()
if err != nil {
log.Errorf("failed generating Relay token: %v", err)
}
}
settings, err := s.settingsManager.GetSettings(ctx, peer.AccountID, activity.SystemInitiator)
if err != nil {
log.WithContext(ctx).Warnf("failed getting settings for peer %s: %s", peer.Key, err)
return nil, status.Errorf(codes.Internal, "failed getting settings")
}
// if peer has reached this point then it has logged in
loginResp := &proto.LoginResponse{
NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil),
PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain(settings), false),
Checks: toProtocolChecks(ctx, postureChecks),
}
return loginResp, nil
}
// processJwtToken validates the existence of a JWT token in the login request, and returns the corresponding user ID if
// the token is valid.
//
@@ -712,7 +730,7 @@ func (s *GRPCServer) sendInitialSync(ctx context.Context, peerKey wgtypes.Key, p
return status.Errorf(codes.Internal, "error handling request")
}
plainResp := toSyncResponse(ctx, s.config, peer, turnToken, relayToken, networkMap, s.accountManager.GetDNSDomain(), postureChecks, nil, settings.RoutingPeerDNSResolutionEnabled, settings.Extra)
plainResp := toSyncResponse(ctx, s.config, peer, turnToken, relayToken, networkMap, s.accountManager.GetDNSDomain(settings), postureChecks, nil, settings.RoutingPeerDNSResolutionEnabled, settings.Extra)
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, plainResp)
if err != nil {
+135 -8
View File
@@ -43,9 +43,30 @@ components:
example: ch8i4ug6lnn4g9hqv7l0
settings:
$ref: '#/components/schemas/AccountSettings'
domain:
description: Account domain
type: string
example: netbird.io
domain_category:
description: Account domain category
type: string
example: private
created_at:
description: Account creation date (UTC)
type: string
format: date-time
example: "2023-05-05T09:00:35.477782Z"
created_by:
description: Account creator
type: string
example: google-oauth2|277474792786460067937
required:
- id
- settings
- domain
- domain_category
- created_at
- created_by
AccountSettings:
type: object
properties:
@@ -91,6 +112,10 @@ components:
description: Enables or disables DNS resolution on the routing peers
type: boolean
example: true
dns_domain:
description: Allows to define a custom dns domain for the account
type: string
example: my-organization.org
extra:
$ref: '#/components/schemas/AccountExtraSettings'
required:
@@ -191,11 +216,25 @@ components:
UserPermissions:
type: object
properties:
dashboard_view:
description: User's permission to view the dashboard
type: string
enum: [ "limited", "blocked", "full" ]
example: limited
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:
@@ -1990,6 +2029,32 @@ components:
- policy_name
- icmp_type
- icmp_code
NetworkTrafficEventsResponse:
type: object
properties:
data:
type: array
description: List of network traffic events
items:
$ref: "#/components/schemas/NetworkTrafficEvent"
page:
type: integer
description: Current page number
page_size:
type: integer
description: Number of items per page
total_records:
type: integer
description: Total number of event records available
total_pages:
type: integer
description: Total number of pages available
required:
- data
- page
- page_size
- total_records
- total_pages
responses:
not_found:
description: Resource not found
@@ -4206,15 +4271,77 @@ paths:
tags: [ Events ]
x-cloud-only: true
x-experimental: true
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 items per page
required: false
schema:
type: integer
minimum: 1
maximum: 50000
default: 1000
- name: user_id
in: query
description: Filter by user ID
required: false
schema:
type: string
- name: protocol
in: query
description: Filter by protocol
required: false
schema:
type: integer
- name: type
in: query
description: Filter by event type
required: false
schema:
type: string
enum: [TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP]
- name: direction
in: query
description: Filter by direction
required: false
schema:
type: string
enum: [INGRESS, EGRESS, DIRECTION_UNKNOWN]
- name: search
in: query
description: Filters events with a partial match on user email, source and destination names and source and destination addresses
required: false
schema:
type: string
- name: start_date
in: query
description: Start date for filtering events (ISO 8601 format, e.g., 2024-01-01T00:00:00Z).
required: false
schema:
type: string
format: date-time
- name: end_date
in: query
description: End date for filtering events (ISO 8601 format, e.g., 2024-01-31T23:59:59Z).
required: false
schema:
type: string
format: date-time
responses:
"200":
description: List of network traffic events
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/NetworkTrafficEvent"
$ref: "#/components/schemas/NetworkTrafficEventsResponse"
'400':
"$ref": "#/components/responses/bad_request"
'401':
+84 -10
View File
@@ -178,11 +178,19 @@ const (
UserStatusInvited UserStatus = "invited"
)
// Defines values for UserPermissionsDashboardView.
// Defines values for GetApiEventsNetworkTrafficParamsType.
const (
UserPermissionsDashboardViewBlocked UserPermissionsDashboardView = "blocked"
UserPermissionsDashboardViewFull UserPermissionsDashboardView = "full"
UserPermissionsDashboardViewLimited UserPermissionsDashboardView = "limited"
GetApiEventsNetworkTrafficParamsTypeTYPEDROP GetApiEventsNetworkTrafficParamsType = "TYPE_DROP"
GetApiEventsNetworkTrafficParamsTypeTYPEEND GetApiEventsNetworkTrafficParamsType = "TYPE_END"
GetApiEventsNetworkTrafficParamsTypeTYPESTART GetApiEventsNetworkTrafficParamsType = "TYPE_START"
GetApiEventsNetworkTrafficParamsTypeTYPEUNKNOWN GetApiEventsNetworkTrafficParamsType = "TYPE_UNKNOWN"
)
// Defines values for GetApiEventsNetworkTrafficParamsDirection.
const (
GetApiEventsNetworkTrafficParamsDirectionDIRECTIONUNKNOWN GetApiEventsNetworkTrafficParamsDirection = "DIRECTION_UNKNOWN"
GetApiEventsNetworkTrafficParamsDirectionEGRESS GetApiEventsNetworkTrafficParamsDirection = "EGRESS"
GetApiEventsNetworkTrafficParamsDirectionINGRESS GetApiEventsNetworkTrafficParamsDirection = "INGRESS"
)
// AccessiblePeer defines model for AccessiblePeer.
@@ -223,6 +231,18 @@ type AccessiblePeer struct {
// Account defines model for Account.
type Account struct {
// CreatedAt Account creation date (UTC)
CreatedAt time.Time `json:"created_at"`
// CreatedBy Account creator
CreatedBy string `json:"created_by"`
// Domain Account domain
Domain string `json:"domain"`
// DomainCategory Account domain category
DomainCategory string `json:"domain_category"`
// Id Account ID
Id string `json:"id"`
Settings AccountSettings `json:"settings"`
@@ -247,7 +267,9 @@ type AccountRequest struct {
// AccountSettings defines model for AccountSettings.
type AccountSettings struct {
Extra *AccountExtraSettings `json:"extra,omitempty"`
// DnsDomain Allows to define a custom dns domain for the account
DnsDomain *string `json:"dns_domain,omitempty"`
Extra *AccountExtraSettings `json:"extra,omitempty"`
// GroupsPropagationEnabled Allows propagate the new user auto groups to peers that belongs to the user
GroupsPropagationEnabled *bool `json:"groups_propagation_enabled,omitempty"`
@@ -908,6 +930,24 @@ type NetworkTrafficEvent struct {
UserName *string `json:"user_name"`
}
// NetworkTrafficEventsResponse defines model for NetworkTrafficEventsResponse.
type NetworkTrafficEventsResponse struct {
// Data List of network traffic events
Data []NetworkTrafficEvent `json:"data"`
// Page Current page number
Page int `json:"page"`
// PageSize Number of items per page
PageSize int `json:"page_size"`
// TotalPages Total number of pages available
TotalPages int `json:"total_pages"`
// TotalRecords Total number of event records available
TotalRecords int `json:"total_records"`
}
// NetworkTrafficLocation defines model for NetworkTrafficLocation.
type NetworkTrafficLocation struct {
// CityName Name of the city (if known).
@@ -1710,13 +1750,11 @@ type UserCreateRequest struct {
// UserPermissions defines model for UserPermissions.
type UserPermissions struct {
// DashboardView User's permission to view the dashboard
DashboardView *UserPermissionsDashboardView `json:"dashboard_view,omitempty"`
// IsRestricted Indicates whether this User's Peers view is restricted
IsRestricted bool `json:"is_restricted"`
Modules map[string]map[string]bool `json:"modules"`
}
// UserPermissionsDashboardView User's permission to view the dashboard
type UserPermissionsDashboardView string
// UserRequest defines model for UserRequest.
type UserRequest struct {
// AutoGroups Group IDs to auto-assign to peers registered by this user
@@ -1729,6 +1767,42 @@ type UserRequest struct {
Role string `json:"role"`
}
// GetApiEventsNetworkTrafficParams defines parameters for GetApiEventsNetworkTraffic.
type GetApiEventsNetworkTrafficParams struct {
// Page Page number
Page *int `form:"page,omitempty" json:"page,omitempty"`
// PageSize Number of items per page
PageSize *int `form:"page_size,omitempty" json:"page_size,omitempty"`
// UserId Filter by user ID
UserId *string `form:"user_id,omitempty" json:"user_id,omitempty"`
// Protocol Filter by protocol
Protocol *int `form:"protocol,omitempty" json:"protocol,omitempty"`
// Type Filter by event type
Type *GetApiEventsNetworkTrafficParamsType `form:"type,omitempty" json:"type,omitempty"`
// Direction Filter by direction
Direction *GetApiEventsNetworkTrafficParamsDirection `form:"direction,omitempty" json:"direction,omitempty"`
// Search Filters events with a partial match on user email, source and destination names and source and destination addresses
Search *string `form:"search,omitempty" json:"search,omitempty"`
// StartDate Start date for filtering events (ISO 8601 format, e.g., 2024-01-01T00:00:00Z).
StartDate *time.Time `form:"start_date,omitempty" json:"start_date,omitempty"`
// EndDate End date for filtering events (ISO 8601 format, e.g., 2024-01-31T23:59:59Z).
EndDate *time.Time `form:"end_date,omitempty" json:"end_date,omitempty"`
}
// GetApiEventsNetworkTrafficParamsType defines parameters for GetApiEventsNetworkTraffic.
type GetApiEventsNetworkTrafficParamsType string
// GetApiEventsNetworkTrafficParamsDirection defines parameters for GetApiEventsNetworkTraffic.
type GetApiEventsNetworkTrafficParamsDirection string
// GetApiPeersParams defines parameters for GetApiPeers.
type GetApiPeersParams struct {
// Name Filter peers by name
+1
View File
@@ -62,6 +62,7 @@ func NewAPIHandler(
authManager,
accountManager.GetAccountIDFromUserAuth,
accountManager.SyncUserJWTGroups,
accountManager.GetUserFromUserAuth,
)
corsMiddleware := cors.AllowAll()
@@ -47,13 +47,19 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) {
accountID, userID := userAuth.AccountId, userAuth.UserId
meta, err := h.accountManager.GetAccountMeta(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
settings, err := h.settingsManager.GetSettings(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
resp := toAccountResponse(accountID, settings)
resp := toAccountResponse(accountID, settings, meta)
util.WriteJSONObject(r.Context(), w, []*api.Account{resp})
}
@@ -113,6 +119,9 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
if req.Settings.RoutingPeerDnsResolutionEnabled != nil {
settings.RoutingPeerDNSResolutionEnabled = *req.Settings.RoutingPeerDnsResolutionEnabled
}
if req.Settings.DnsDomain != nil {
settings.DNSDomain = *req.Settings.DnsDomain
}
updatedAccount, err := h.accountManager.UpdateAccountSettings(r.Context(), accountID, userID, settings)
if err != nil {
@@ -120,7 +129,13 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
return
}
resp := toAccountResponse(updatedAccount.Id, updatedAccount.Settings)
meta, err := h.accountManager.GetAccountMeta(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
resp := toAccountResponse(updatedAccount.Id, updatedAccount.Settings, meta)
util.WriteJSONObject(r.Context(), w, &resp)
}
@@ -149,7 +164,7 @@ func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request) {
util.WriteJSONObject(r.Context(), w, util.EmptyObject{})
}
func toAccountResponse(accountID string, settings *types.Settings) *api.Account {
func toAccountResponse(accountID string, settings *types.Settings, meta *types.AccountMeta) *api.Account {
jwtAllowGroups := settings.JWTAllowGroups
if jwtAllowGroups == nil {
jwtAllowGroups = []string{}
@@ -166,6 +181,7 @@ func toAccountResponse(accountID string, settings *types.Settings) *api.Account
JwtAllowGroups: &jwtAllowGroups,
RegularUsersViewBlocked: settings.RegularUsersViewBlocked,
RoutingPeerDnsResolutionEnabled: &settings.RoutingPeerDNSResolutionEnabled,
DnsDomain: &settings.DNSDomain,
}
if settings.Extra != nil {
@@ -177,7 +193,11 @@ func toAccountResponse(accountID string, settings *types.Settings) *api.Account
}
return &api.Account{
Id: accountID,
Settings: apiSettings,
Id: accountID,
Settings: apiSettings,
CreatedAt: meta.CreatedAt,
CreatedBy: meta.CreatedBy,
Domain: meta.Domain,
DomainCategory: meta.DomainCategory,
}
}
@@ -50,6 +50,12 @@ func initAccountsTestData(t *testing.T, account *types.Account) *handler {
accCopy.UpdateSettings(newSettings)
return accCopy, nil
},
GetAccountByIDFunc: func(ctx context.Context, accountID string, userID string) (*types.Account, error) {
return account.Copy(), nil
},
GetAccountMetaFunc: func(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error) {
return account.GetMeta(), nil
},
},
settingsManager: settingsMockManager,
}
@@ -102,6 +108,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
JwtAllowGroups: &[]string{},
RegularUsersViewBlocked: true,
RoutingPeerDnsResolutionEnabled: br(false),
DnsDomain: sr(""),
},
expectedArray: true,
expectedID: accountID,
@@ -122,6 +129,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
JwtAllowGroups: &[]string{},
RegularUsersViewBlocked: false,
RoutingPeerDnsResolutionEnabled: br(false),
DnsDomain: sr(""),
},
expectedArray: false,
expectedID: accountID,
@@ -142,6 +150,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
JwtAllowGroups: &[]string{"test"},
RegularUsersViewBlocked: true,
RoutingPeerDnsResolutionEnabled: br(false),
DnsDomain: sr(""),
},
expectedArray: false,
expectedID: accountID,
@@ -162,6 +171,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
JwtAllowGroups: &[]string{},
RegularUsersViewBlocked: true,
RoutingPeerDnsResolutionEnabled: br(false),
DnsDomain: sr(""),
},
expectedArray: false,
expectedID: accountID,
@@ -65,7 +65,13 @@ func (h *Handler) getPeer(ctx context.Context, accountID, peerID, userID string,
util.WriteError(ctx, err, w)
return
}
dnsDomain := h.accountManager.GetDNSDomain()
settings, err := h.accountManager.GetAccountSettings(ctx, accountID, activity.SystemInitiator)
if err != nil {
util.WriteError(ctx, err, w)
return
}
dnsDomain := h.accountManager.GetDNSDomain(settings)
grps, _ := h.accountManager.GetPeerGroups(ctx, accountID, peerID)
grpsInfoMap := groups.ToGroupsInfoMap(grps, 0)
@@ -110,7 +116,13 @@ func (h *Handler) updatePeer(ctx context.Context, accountID, userID, peerID stri
util.WriteError(ctx, err, w)
return
}
dnsDomain := h.accountManager.GetDNSDomain()
settings, err := h.accountManager.GetAccountSettings(ctx, accountID, activity.SystemInitiator)
if err != nil {
util.WriteError(ctx, err, w)
return
}
dnsDomain := h.accountManager.GetDNSDomain(settings)
peerGroups, err := h.accountManager.GetPeerGroups(ctx, accountID, peer.ID)
if err != nil {
@@ -192,7 +204,12 @@ func (h *Handler) GetAllPeers(w http.ResponseWriter, r *http.Request) {
return
}
dnsDomain := h.accountManager.GetDNSDomain()
settings, err := h.accountManager.GetAccountSettings(r.Context(), accountID, activity.SystemInitiator)
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
dnsDomain := h.accountManager.GetDNSDomain(settings)
grps, _ := h.accountManager.GetAllGroups(r.Context(), accountID, userID)
@@ -279,7 +296,7 @@ func (h *Handler) GetAccessiblePeers(w http.ResponseWriter, r *http.Request) {
return
}
dnsDomain := h.accountManager.GetDNSDomain()
dnsDomain := h.accountManager.GetDNSDomain(account.Settings)
customZone := account.GetPeersCustomZone(r.Context(), dnsDomain)
netMap := account.GetPeerNetworkMap(r.Context(), peerID, customZone, validPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil)
@@ -152,7 +152,7 @@ func initTestMetaData(peers ...*nbpeer.Peer) *Handler {
},
}, nil
},
GetDNSDomainFunc: func() string {
GetDNSDomainFunc: func(settings *types.Settings) string {
return "netbird.selfhosted"
},
GetAccountFunc: func(ctx context.Context, accountID string) (*types.Account, error) {
@@ -172,6 +172,9 @@ func initTestMetaData(peers ...*nbpeer.Peer) *Handler {
_, ok := statuses[peerID]
return ok
},
GetAccountSettingsFunc: func(ctx context.Context, accountID string, userID string) (*types.Settings, error) {
return account.Settings, nil
},
},
}
}
@@ -13,6 +13,7 @@ import (
"github.com/netbirdio/netbird/management/server/http/util"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/users"
nbcontext "github.com/netbirdio/netbird/management/server/context"
)
@@ -272,15 +273,33 @@ func (h *handler) getCurrentUser(w http.ResponseWriter, r *http.Request) {
return
}
accountID, userID := userAuth.AccountId, userAuth.UserId
user, err := h.accountManager.GetCurrentUserInfo(ctx, accountID, userID)
user, err := h.accountManager.GetCurrentUserInfo(ctx, userAuth)
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
util.WriteJSONObject(r.Context(), w, toUserResponse(user, userID))
util.WriteJSONObject(r.Context(), w, toUserWithPermissionsResponse(user, userAuth.UserId))
}
func toUserWithPermissionsResponse(user *users.UserInfoWithPermissions, userID string) *api.User {
response := toUserResponse(user.UserInfo, userID)
// stringify modules and operations keys
modules := make(map[string]map[string]bool)
for module, operations := range user.Permissions {
modules[string(module)] = make(map[string]bool)
for op, val := range operations {
modules[string(module)][string(op)] = val
}
}
response.Permissions = &api.UserPermissions{
IsRestricted: user.Restricted,
Modules: modules,
}
return response
}
func toUserResponse(user *types.UserInfo, currenUserID string) *api.User {
@@ -316,8 +335,5 @@ func toUserResponse(user *types.UserInfo, currenUserID string) *api.User {
IsBlocked: user.IsBlocked,
LastLogin: &user.LastLogin,
Issued: &user.Issued,
Permissions: &api.UserPermissions{
DashboardView: (*api.UserPermissionsDashboardView)(&user.Permissions.DashboardView),
},
}
}
@@ -13,12 +13,16 @@ import (
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/permissions/modules"
"github.com/netbirdio/netbird/management/server/permissions/roles"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/users"
)
const (
@@ -107,7 +111,7 @@ func initUsersTestData() *handler {
return nil, status.Errorf(status.NotFound, "user with ID %s does not exists", userID)
}
info, err := update.Copy().ToUserInfo(nil, &types.Settings{RegularUsersViewBlocked: false})
info, err := update.Copy().ToUserInfo(nil)
if err != nil {
return nil, err
}
@@ -124,8 +128,8 @@ func initUsersTestData() *handler {
return nil
},
GetCurrentUserInfoFunc: func(ctx context.Context, accountID, userID string) (*types.UserInfo, error) {
switch userID {
GetCurrentUserInfoFunc: func(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error) {
switch userAuth.UserId {
case "not-found":
return nil, status.NewUserNotFoundError("not-found")
case "not-of-account":
@@ -135,52 +139,68 @@ func initUsersTestData() *handler {
case "service-user":
return nil, status.NewPermissionDeniedError()
case "owner":
return &types.UserInfo{
ID: "owner",
Name: "",
Role: "owner",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
Issued: "api",
Permissions: types.UserPermissions{
DashboardView: "full",
return &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "owner",
Name: "",
Role: "owner",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
Issued: "api",
},
Permissions: mergeRolePermissions(roles.Owner),
}, nil
case "regular-user":
return &types.UserInfo{
ID: "regular-user",
Name: "",
Role: "user",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
Issued: "api",
Permissions: types.UserPermissions{
DashboardView: "limited",
return &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "regular-user",
Name: "",
Role: "user",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
Issued: "api",
},
Permissions: mergeRolePermissions(roles.User),
}, nil
case "admin-user":
return &types.UserInfo{
ID: "admin-user",
Name: "",
Role: "admin",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
Permissions: types.UserPermissions{
DashboardView: "full",
return &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "admin-user",
Name: "",
Role: "admin",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
},
Permissions: mergeRolePermissions(roles.Admin),
}, nil
case "restricted-user":
return &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "restricted-user",
Name: "",
Role: "user",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
},
Permissions: mergeRolePermissions(roles.User),
Restricted: true,
}, nil
}
return nil, fmt.Errorf("user id %s not handled", userID)
return nil, fmt.Errorf("user id %s not handled", userAuth.UserId)
},
},
}
@@ -546,6 +566,7 @@ func TestCurrentUser(t *testing.T) {
name string
expectedStatus int
requestAuth nbcontext.UserAuth
expectedResult *api.User
}{
{
name: "without auth",
@@ -575,16 +596,78 @@ func TestCurrentUser(t *testing.T) {
name: "owner",
requestAuth: nbcontext.UserAuth{UserId: "owner"},
expectedStatus: http.StatusOK,
expectedResult: &api.User{
Id: "owner",
Role: "owner",
Status: "active",
IsBlocked: false,
IsCurrent: ptr(true),
IsServiceUser: ptr(false),
AutoGroups: []string{},
Issued: ptr("api"),
LastLogin: ptr(time.Time{}),
Permissions: &api.UserPermissions{
Modules: stringifyPermissionsKeys(mergeRolePermissions(roles.Owner)),
},
},
},
{
name: "regular user",
requestAuth: nbcontext.UserAuth{UserId: "regular-user"},
expectedStatus: http.StatusOK,
expectedResult: &api.User{
Id: "regular-user",
Role: "user",
Status: "active",
IsBlocked: false,
IsCurrent: ptr(true),
IsServiceUser: ptr(false),
AutoGroups: []string{},
Issued: ptr("api"),
LastLogin: ptr(time.Time{}),
Permissions: &api.UserPermissions{
Modules: stringifyPermissionsKeys(mergeRolePermissions(roles.User)),
},
},
},
{
name: "admin user",
requestAuth: nbcontext.UserAuth{UserId: "admin-user"},
expectedStatus: http.StatusOK,
expectedResult: &api.User{
Id: "admin-user",
Role: "admin",
Status: "active",
IsBlocked: false,
IsCurrent: ptr(true),
IsServiceUser: ptr(false),
AutoGroups: []string{},
Issued: ptr("api"),
LastLogin: ptr(time.Time{}),
Permissions: &api.UserPermissions{
Modules: stringifyPermissionsKeys(mergeRolePermissions(roles.Admin)),
},
},
},
{
name: "restricted user",
requestAuth: nbcontext.UserAuth{UserId: "restricted-user"},
expectedStatus: http.StatusOK,
expectedResult: &api.User{
Id: "restricted-user",
Role: "user",
Status: "active",
IsBlocked: false,
IsCurrent: ptr(true),
IsServiceUser: ptr(false),
AutoGroups: []string{},
Issued: ptr("api"),
LastLogin: ptr(time.Time{}),
Permissions: &api.UserPermissions{
IsRestricted: true,
Modules: stringifyPermissionsKeys(mergeRolePermissions(roles.User)),
},
},
},
}
@@ -603,10 +686,42 @@ func TestCurrentUser(t *testing.T) {
res := rr.Result()
defer res.Body.Close()
if status := rr.Code; status != tc.expectedStatus {
t.Fatalf("handler returned wrong status code: got %v want %v",
status, tc.expectedStatus)
assert.Equal(t, tc.expectedStatus, rr.Code, "handler returned wrong status code")
if tc.expectedResult != nil {
var result api.User
require.NoError(t, json.NewDecoder(res.Body).Decode(&result))
assert.EqualValues(t, *tc.expectedResult, result)
}
})
}
}
func ptr[T any, PT *T](x T) PT {
return &x
}
func mergeRolePermissions(role roles.RolePermissions) roles.Permissions {
permissions := roles.Permissions{}
for k := range modules.All {
if rolePermissions, ok := role.Permissions[k]; ok {
permissions[k] = rolePermissions
continue
}
permissions[k] = role.AutoAllowNew
}
return permissions
}
func stringifyPermissionsKeys(permissions roles.Permissions) map[string]map[string]bool {
modules := make(map[string]map[string]bool)
for module, operations := range permissions {
modules[string(module)] = make(map[string]bool)
for op, val := range operations {
modules[string(module)][string(op)] = val
}
}
return modules
}
@@ -15,16 +15,20 @@ import (
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
"github.com/netbirdio/netbird/management/server/http/util"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
)
type EnsureAccountFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
type SyncUserJWTGroupsFunc func(ctx context.Context, userAuth nbcontext.UserAuth) error
type GetUserFromUserAuthFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error)
// AuthMiddleware middleware to verify personal access tokens (PAT) and JWT tokens
type AuthMiddleware struct {
authManager auth.Manager
ensureAccount EnsureAccountFunc
syncUserJWTGroups SyncUserJWTGroupsFunc
authManager auth.Manager
ensureAccount EnsureAccountFunc
getUserFromUserAuth GetUserFromUserAuthFunc
syncUserJWTGroups SyncUserJWTGroupsFunc
}
// NewAuthMiddleware instance constructor
@@ -32,11 +36,13 @@ func NewAuthMiddleware(
authManager auth.Manager,
ensureAccount EnsureAccountFunc,
syncUserJWTGroups SyncUserJWTGroupsFunc,
getUserFromUserAuth GetUserFromUserAuthFunc,
) *AuthMiddleware {
return &AuthMiddleware{
authManager: authManager,
ensureAccount: ensureAccount,
syncUserJWTGroups: syncUserJWTGroups,
authManager: authManager,
ensureAccount: ensureAccount,
syncUserJWTGroups: syncUserJWTGroups,
getUserFromUserAuth: getUserFromUserAuth,
}
}
@@ -123,6 +129,12 @@ func (m *AuthMiddleware) checkJWTFromRequest(r *http.Request, auth []string) (*h
log.WithContext(ctx).Errorf("HTTP server failed to sync user JWT groups: %s", err)
}
_, err = m.getUserFromUserAuth(ctx, userAuth)
if err != nil {
log.WithContext(ctx).Errorf("HTTP server failed to update user from user auth: %s", err)
return r, err
}
return nbcontext.SetUserAuthInRequest(r, userAuth), nil
}
@@ -155,6 +167,11 @@ func (m *AuthMiddleware) checkPATFromRequest(r *http.Request, auth []string) (*h
IsPAT: true,
}
if impersonate, ok := r.URL.Query()["account"]; ok && len(impersonate) == 1 {
userAuth.AccountId = impersonate[0]
userAuth.IsChild = ok
}
return nbcontext.SetUserAuthInRequest(r, userAuth), nil
}
@@ -190,6 +190,9 @@ func TestAuthMiddleware_Handler(t *testing.T) {
func(ctx context.Context, userAuth nbcontext.UserAuth) error {
return nil
},
func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) {
return &types.User{}, nil
},
)
handlerToTest := authMiddleware.Handler(nextHandler)
@@ -239,14 +242,15 @@ func TestAuthMiddleware_Handler_Child(t *testing.T) {
},
},
{
name: "Valid PAT Token ignores child",
name: "Valid PAT Token accesses child",
path: "/test?account=xyz",
authHeader: "Token " + PAT,
expectedUserAuth: &nbcontext.UserAuth{
AccountId: accountID,
AccountId: "xyz",
UserId: userID,
Domain: testAccount.Domain,
DomainCategory: testAccount.DomainCategory,
IsChild: true,
IsPAT: true,
},
},
@@ -291,6 +295,9 @@ func TestAuthMiddleware_Handler_Child(t *testing.T) {
func(ctx context.Context, userAuth nbcontext.UserAuth) error {
return nil
},
func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) {
return &types.User{}, nil
},
)
for _, tc := range tt {
+21
View File
@@ -352,3 +352,24 @@ func MigrateNewField[T any](ctx context.Context, db *gorm.DB, columnName string,
log.WithContext(ctx).Infof("Migration of empty %s to default value in table %s completed", columnName, tableName)
return nil
}
func DropIndex[T any](ctx context.Context, db *gorm.DB, indexName string) error {
var model T
if !db.Migrator().HasTable(&model) {
log.WithContext(ctx).Debugf("table for %T does not exist, no migration needed", model)
return nil
}
if !db.Migrator().HasIndex(&model, indexName) {
log.WithContext(ctx).Debugf("index %s does not exist in table %T, no migration needed", indexName, model)
return nil
}
if err := db.Migrator().DropIndex(&model, indexName); err != nil {
return fmt.Errorf("failed to drop index %s: %w", indexName, err)
}
log.WithContext(ctx).Infof("dropped index %s from table %T", indexName, model)
return nil
}
@@ -227,3 +227,25 @@ func TestMigrateSetupKeyToHashedSetupKey_ForAlreadyMigratedKey_Case2(t *testing.
assert.Equal(t, "9+FQcmNd2GCxIK+SvHmtp6PPGV4MKEicDS+xuSQmvlE=", key.Key, "Key should be hashed")
}
func TestDropIndex(t *testing.T) {
db := setupDatabase(t)
err := db.AutoMigrate(&types.SetupKey{})
require.NoError(t, err, "Failed to auto-migrate tables")
err = db.Save(&types.SetupKey{
Id: "1",
Key: "9+FQcmNd2GCxIK+SvHmtp6PPGV4MKEicDS+xuSQmvlE=",
}).Error
require.NoError(t, err, "Failed to insert setup key")
exist := db.Migrator().HasIndex(&types.SetupKey{}, "idx_setup_keys_account_id")
assert.True(t, exist, "Should have the index")
err = migration.DropIndex[types.SetupKey](context.Background(), db, "idx_setup_keys_account_id")
require.NoError(t, err, "Migration should not fail to remove index")
exist = db.Migrator().HasIndex(&types.SetupKey{}, "idx_setup_keys_account_id")
assert.False(t, exist, "Should not have the index")
}
+16 -6
View File
@@ -19,6 +19,7 @@ import (
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/users"
"github.com/netbirdio/netbird/route"
)
@@ -83,7 +84,7 @@ type MockAccountManager struct {
CreateUserFunc func(ctx context.Context, accountID, userID string, key *types.UserInfo) (*types.UserInfo, error)
GetAccountIDFromUserAuthFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
DeleteAccountFunc func(ctx context.Context, accountID, userID string) error
GetDNSDomainFunc func() string
GetDNSDomainFunc func(settings *types.Settings) string
StoreEventFunc func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any)
GetEventsFunc func(ctx context.Context, accountID, userID string) ([]*activity.Event, error)
GetDNSSettingsFunc func(ctx context.Context, accountID, userID string) (*types.DNSSettings, error)
@@ -115,7 +116,8 @@ type MockAccountManager struct {
CreateAccountByPrivateDomainFunc func(ctx context.Context, initiatorId, domain string) (*types.Account, error)
UpdateToPrimaryAccountFunc func(ctx context.Context, accountId string) (*types.Account, error)
GetOwnerInfoFunc func(ctx context.Context, accountID string) (*types.UserInfo, error)
GetCurrentUserInfoFunc func(ctx context.Context, accountID, userID string) (*types.UserInfo, error)
GetCurrentUserInfoFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error)
GetAccountMetaFunc func(ctx context.Context, accountID, userID string) (*types.AccountMeta, error)
}
func (am *MockAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) {
@@ -619,9 +621,9 @@ func (am *MockAccountManager) GetPeers(ctx context.Context, accountID, userID, n
}
// GetDNSDomain mocks GetDNSDomain of the AccountManager interface
func (am *MockAccountManager) GetDNSDomain() string {
func (am *MockAccountManager) GetDNSDomain(settings *types.Settings) string {
if am.GetDNSDomainFunc != nil {
return am.GetDNSDomainFunc()
return am.GetDNSDomainFunc(settings)
}
return ""
}
@@ -803,6 +805,14 @@ func (am *MockAccountManager) GetAccountByID(ctx context.Context, accountID stri
return nil, status.Errorf(codes.Unimplemented, "method GetAccountByID is not implemented")
}
// GetAccountByID mocks GetAccountByID of the AccountManager interface
func (am *MockAccountManager) GetAccountMeta(ctx context.Context, accountID string, userID string) (*types.AccountMeta, error) {
if am.GetAccountMetaFunc != nil {
return am.GetAccountMetaFunc(ctx, accountID, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method GetAccountMeta is not implemented")
}
// GetUserByID mocks GetUserByID of the AccountManager interface
func (am *MockAccountManager) GetUserByID(ctx context.Context, id string) (*types.User, error) {
if am.GetUserByIDFunc != nil {
@@ -873,9 +883,9 @@ func (am *MockAccountManager) GetOwnerInfo(ctx context.Context, accountId string
return nil, status.Errorf(codes.Unimplemented, "method GetOwnerInfo is not implemented")
}
func (am *MockAccountManager) GetCurrentUserInfo(ctx context.Context, accountID, userID string) (*types.UserInfo, error) {
func (am *MockAccountManager) GetCurrentUserInfo(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error) {
if am.GetCurrentUserInfoFunc != nil {
return am.GetCurrentUserInfoFunc(ctx, accountID, userID)
return am.GetCurrentUserInfoFunc(ctx, userAuth)
}
return nil, status.Errorf(codes.Unimplemented, "method GetCurrentUserInfo is not implemented")
}
@@ -30,7 +30,7 @@ func (p NetworkResourceType) String() string {
}
type NetworkResource struct {
ID string `gorm:"index"`
ID string `gorm:"primaryKey"`
NetworkID string `gorm:"index"`
AccountID string `gorm:"index"`
Name string
@@ -10,7 +10,7 @@ import (
)
type NetworkRouter struct {
ID string `gorm:"index"`
ID string `gorm:"primaryKey"`
NetworkID string `gorm:"index"`
AccountID string `gorm:"index"`
Peer string
+1 -1
View File
@@ -7,7 +7,7 @@ import (
)
type Network struct {
ID string `gorm:"index"`
ID string `gorm:"primaryKey"`
AccountID string `gorm:"index"`
Name string
Description string
+50 -32
View File
@@ -49,20 +49,9 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID
return nil, err
}
peers := make([]*nbpeer.Peer, 0)
peersMap := make(map[string]*nbpeer.Peer)
for _, peer := range accountPeers {
if user.IsRegularUser() && user.Id != peer.UserID {
// only display peers that belong to the current user if the current user is not an admin
continue
}
peers = append(peers, peer)
peersMap[peer.ID] = peer
}
// @note if the user has permission to read peers it shows all account peers
if allowed {
return peers, nil
return accountPeers, nil
}
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
@@ -70,10 +59,22 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID
return nil, fmt.Errorf("failed to get account settings: %w", err)
}
if settings.RegularUsersViewBlocked {
if user.IsRestrictable() && settings.RegularUsersViewBlocked {
return []*nbpeer.Peer{}, nil
}
// @note if it does not have permission read peers then only display it's own peers
peers := make([]*nbpeer.Peer, 0)
peersMap := make(map[string]*nbpeer.Peer)
for _, peer := range accountPeers {
if user.Id != peer.UserID {
continue
}
peers = append(peers, peer)
peersMap[peer.ID] = peer
}
return am.getUserAccessiblePeers(ctx, accountID, peersMap, peers)
}
@@ -206,6 +207,7 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
var sshChanged bool
var loginExpirationChanged bool
var inactivityExpirationChanged bool
var dnsDomain string
err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
peer, err = transaction.GetPeerByID(ctx, store.LockingStrengthUpdate, accountID, update.ID)
@@ -223,7 +225,9 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
return err
}
update, requiresPeerUpdates, err = am.integratedPeerValidator.ValidatePeer(ctx, update, peer, userID, accountID, am.GetDNSDomain(), peerGroupList, settings.Extra)
dnsDomain = am.GetDNSDomain(settings)
update, requiresPeerUpdates, err = am.integratedPeerValidator.ValidatePeer(ctx, update, peer, userID, accountID, dnsDomain, peerGroupList, settings.Extra)
if err != nil {
return err
}
@@ -276,11 +280,11 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
if !peer.SSHEnabled {
event = activity.PeerSSHDisabled
}
am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(am.GetDNSDomain()))
am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(dnsDomain))
}
if peerLabelChanged {
am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRenamed, peer.EventMeta(am.GetDNSDomain()))
am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRenamed, peer.EventMeta(dnsDomain))
}
if loginExpirationChanged {
@@ -288,7 +292,7 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
if !peer.LoginExpirationEnabled {
event = activity.PeerLoginExpirationDisabled
}
am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(am.GetDNSDomain()))
am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(dnsDomain))
if peer.AddedWithSSOLogin() && peer.LoginExpirationEnabled && settings.PeerLoginExpirationEnabled {
am.checkAndSchedulePeerLoginExpiration(ctx, accountID)
@@ -300,7 +304,7 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
if !peer.InactivityExpirationEnabled {
event = activity.PeerInactivityExpirationDisabled
}
am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(am.GetDNSDomain()))
am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(dnsDomain))
if peer.AddedWithSSOLogin() && peer.InactivityExpirationEnabled && settings.PeerInactivityExpirationEnabled {
am.checkAndSchedulePeerInactivityExpiration(ctx, accountID)
@@ -413,7 +417,7 @@ func (am *DefaultAccountManager) GetNetworkMap(ctx context.Context, peerID strin
if err != nil {
return nil, err
}
customZone := account.GetPeersCustomZone(ctx, am.dnsDomain)
customZone := account.GetPeersCustomZone(ctx, am.GetDNSDomain(account.Settings))
proxyNetworkMaps, err := am.proxyController.GetProxyNetworkMaps(ctx, account.Id)
if err != nil {
@@ -574,8 +578,13 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
ExtraDNSLabels: peer.ExtraDNSLabels,
AllowExtraDNSLabels: allowExtraDNSLabels,
}
settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return fmt.Errorf("failed to get account settings: %w", err)
}
opEvent.TargetID = newPeer.ID
opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain())
opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain(settings))
if !addedByUser {
opEvent.Meta["setup_key_name"] = setupKeyName
}
@@ -591,10 +600,6 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
}
}
settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return fmt.Errorf("failed to get account settings: %w", err)
}
newPeer = am.integratedPeerValidator.PreparePeer(ctx, accountID, newPeer, groupsToAdd, settings.Extra)
err = transaction.AddPeerToAllGroup(ctx, store.LockingStrengthUpdate, accountID, newPeer.ID)
@@ -1024,7 +1029,7 @@ func (am *DefaultAccountManager) getValidatedPeerWithMap(ctx context.Context, is
return nil, nil, nil, err
}
customZone := account.GetPeersCustomZone(ctx, am.dnsDomain)
customZone := account.GetPeersCustomZone(ctx, am.GetDNSDomain(account.Settings))
proxyNetworkMaps, err := am.proxyController.GetProxyNetworkMaps(ctx, account.Id)
if err != nil {
@@ -1060,7 +1065,12 @@ func (am *DefaultAccountManager) handleExpiredPeer(ctx context.Context, transact
log.WithContext(ctx).Debugf("failed to update user last login: %v", err)
}
am.StoreEvent(ctx, user.Id, peer.ID, user.AccountID, activity.UserLoggedInPeer, peer.EventMeta(am.GetDNSDomain()))
settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, peer.AccountID)
if err != nil {
return fmt.Errorf("failed to get account settings: %w", err)
}
am.StoreEvent(ctx, user.Id, peer.ID, user.AccountID, activity.UserLoggedInPeer, peer.EventMeta(am.GetDNSDomain(settings)))
return nil
}
@@ -1174,7 +1184,8 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account
semaphore := make(chan struct{}, 10)
dnsCache := &DNSConfigCache{}
customZone := account.GetPeersCustomZone(ctx, am.dnsDomain)
dnsDomain := am.GetDNSDomain(account.Settings)
customZone := account.GetPeersCustomZone(ctx, dnsDomain)
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
@@ -1215,7 +1226,7 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account
return
}
update := toSyncResponse(ctx, nil, p, nil, nil, remotePeerNetworkMap, am.GetDNSDomain(), postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled, extraSetting)
update := toSyncResponse(ctx, nil, p, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled, extraSetting)
am.peersUpdateManager.SendUpdate(ctx, p.ID, &UpdateMessage{Update: update, NetworkMap: remotePeerNetworkMap})
}(peer)
}
@@ -1270,7 +1281,8 @@ func (am *DefaultAccountManager) UpdateAccountPeer(ctx context.Context, accountI
}
dnsCache := &DNSConfigCache{}
customZone := account.GetPeersCustomZone(ctx, am.dnsDomain)
dnsDomain := am.GetDNSDomain(account.Settings)
customZone := account.GetPeersCustomZone(ctx, dnsDomain)
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
@@ -1299,7 +1311,7 @@ func (am *DefaultAccountManager) UpdateAccountPeer(ctx context.Context, accountI
return
}
update := toSyncResponse(ctx, nil, peer, nil, nil, remotePeerNetworkMap, am.GetDNSDomain(), postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled, extraSettings)
update := toSyncResponse(ctx, nil, peer, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled, extraSettings)
am.peersUpdateManager.SendUpdate(ctx, peer.ID, &UpdateMessage{Update: update, NetworkMap: remotePeerNetworkMap})
}
@@ -1484,6 +1496,12 @@ func isPeerInActiveGroup(ctx context.Context, transaction store.Store, accountID
func deletePeers(ctx context.Context, am *DefaultAccountManager, transaction store.Store, accountID, userID string, peers []*nbpeer.Peer) ([]func(), error) {
var peerDeletedEvents []func()
settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, err
}
dnsDomain := am.GetDNSDomain(settings)
for _, peer := range peers {
if err := am.integratedPeerValidator.PeerDeleted(ctx, accountID, peer.ID); err != nil {
return nil, err
@@ -1514,7 +1532,7 @@ func deletePeers(ctx context.Context, am *DefaultAccountManager, transaction sto
})
am.peersUpdateManager.CloseChannel(ctx, peer.ID)
peerDeletedEvents = append(peerDeletedEvents, func() {
am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRemovedByUser, peer.EventMeta(am.GetDNSDomain()))
am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRemovedByUser, peer.EventMeta(dnsDomain))
})
}
+21
View File
@@ -20,6 +20,8 @@ type Manager interface {
ValidateUserPermissions(ctx context.Context, accountID, userID string, module modules.Module, operation operations.Operation) (bool, error)
ValidateRoleModuleAccess(ctx context.Context, accountID string, role roles.RolePermissions, module modules.Module, operation operations.Operation) bool
ValidateAccountAccess(ctx context.Context, accountID string, user *types.User, allowOwnerAndAdmin bool) error
GetPermissionsByRole(ctx context.Context, role types.UserRole) (roles.Permissions, error)
}
type managerImpl struct {
@@ -96,3 +98,22 @@ func (m *managerImpl) ValidateAccountAccess(ctx context.Context, accountID strin
}
return nil
}
func (m *managerImpl) GetPermissionsByRole(ctx context.Context, role types.UserRole) (roles.Permissions, error) {
roleMap, ok := roles.RolesMap[role]
if !ok {
return roles.Permissions{}, status.NewUserRoleNotFoundError(string(role))
}
permissions := roles.Permissions{}
for k := range modules.All {
if rolePermissions, ok := roleMap.Permissions[k]; ok {
permissions[k] = rolePermissions
continue
}
permissions[k] = roleMap.AutoAllowNew
}
return permissions, nil
}
@@ -38,6 +38,21 @@ func (m *MockManager) EXPECT() *MockManagerMockRecorder {
return m.recorder
}
// GetPermissionsByRole mocks base method.
func (m *MockManager) GetPermissionsByRole(ctx context.Context, role types.UserRole) (roles.Permissions, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPermissionsByRole", ctx, role)
ret0, _ := ret[0].(roles.Permissions)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPermissionsByRole indicates an expected call of GetPermissionsByRole.
func (mr *MockManagerMockRecorder) GetPermissionsByRole(ctx, role interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPermissionsByRole", reflect.TypeOf((*MockManager)(nil).GetPermissionsByRole), ctx, role)
}
// ValidateAccountAccess mocks base method.
func (m *MockManager) ValidateAccountAccess(ctx context.Context, accountID string, user *types.User, allowOwnerAndAdmin bool) error {
m.ctrl.T.Helper()
@@ -17,3 +17,19 @@ const (
SetupKeys Module = "setup_keys"
Pats Module = "pats"
)
var All = map[Module]struct{}{
Networks: {},
Peers: {},
Groups: {},
Settings: {},
Accounts: {},
Dns: {},
Nameservers: {},
Events: {},
Policies: {},
Routes: {},
Users: {},
SetupKeys: {},
Pats: {},
}
@@ -0,0 +1,16 @@
package roles
import (
"github.com/netbirdio/netbird/management/server/permissions/operations"
"github.com/netbirdio/netbird/management/server/types"
)
var Auditor = RolePermissions{
Role: types.UserRoleAuditor,
AutoAllowNew: map[operations.Operation]bool{
operations.Read: true,
operations.Create: false,
operations.Update: false,
operations.Delete: false,
},
}
@@ -0,0 +1,97 @@
package roles
import (
"github.com/netbirdio/netbird/management/server/permissions/modules"
"github.com/netbirdio/netbird/management/server/permissions/operations"
"github.com/netbirdio/netbird/management/server/types"
)
var NetworkAdmin = RolePermissions{
Role: types.UserRoleNetworkAdmin,
AutoAllowNew: map[operations.Operation]bool{
operations.Read: false,
operations.Create: false,
operations.Update: false,
operations.Delete: false,
},
Permissions: Permissions{
modules.Networks: {
operations.Read: true,
operations.Create: true,
operations.Update: true,
operations.Delete: true,
},
modules.Groups: {
operations.Read: true,
operations.Create: true,
operations.Update: true,
operations.Delete: true,
},
modules.Settings: {
operations.Read: true,
operations.Create: false,
operations.Update: false,
operations.Delete: false,
},
modules.Accounts: {
operations.Read: true,
operations.Create: false,
operations.Update: false,
operations.Delete: false,
},
modules.Dns: {
operations.Read: true,
operations.Create: true,
operations.Update: true,
operations.Delete: true,
},
modules.Nameservers: {
operations.Read: true,
operations.Create: true,
operations.Update: true,
operations.Delete: true,
},
modules.Events: {
operations.Read: true,
operations.Create: false,
operations.Update: false,
operations.Delete: false,
},
modules.Policies: {
operations.Read: true,
operations.Create: true,
operations.Update: true,
operations.Delete: true,
},
modules.Routes: {
operations.Read: true,
operations.Create: true,
operations.Update: true,
operations.Delete: true,
},
modules.Users: {
operations.Read: true,
operations.Create: false,
operations.Update: false,
operations.Delete: false,
},
modules.SetupKeys: {
operations.Read: true,
operations.Create: false,
operations.Update: false,
operations.Delete: false,
},
modules.Pats: {
operations.Read: true,
operations.Create: true,
operations.Update: true,
operations.Delete: true,
},
modules.Peers: {
operations.Read: true,
operations.Create: false,
operations.Update: false,
operations.Delete: false,
},
},
}
@@ -15,7 +15,9 @@ type RolePermissions struct {
type Permissions map[modules.Module]map[operations.Operation]bool
var RolesMap = map[types.UserRole]RolePermissions{
types.UserRoleOwner: Owner,
types.UserRoleAdmin: Admin,
types.UserRoleUser: User,
types.UserRoleOwner: Owner,
types.UserRoleAdmin: Admin,
types.UserRoleUser: User,
types.UserRoleAuditor: Auditor,
types.UserRoleNetworkAdmin: NetworkAdmin,
}
+2
View File
@@ -398,7 +398,9 @@ func toProtocolRoutesFirewallRules(rules []*types.RouteFirewallRule) []*proto.Ro
Protocol: getProtoProtocol(rule.Protocol),
PortInfo: getProtoPortInfo(rule),
IsDynamic: rule.IsDynamic,
Domains: rule.Domains.ToPunycodeList(),
PolicyID: []byte(rule.PolicyID),
RouteID: string(rule.RouteID),
}
}
+19
View File
@@ -1850,6 +1850,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Destination: "192.168.0.0/16",
Protocol: "all",
Port: 80,
RouteID: "route1:peerA",
},
{
SourceRanges: []string{
@@ -1861,6 +1862,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Destination: "192.168.0.0/16",
Protocol: "all",
Port: 320,
RouteID: "route1:peerA",
},
}
additionalFirewallRule := []*types.RouteFirewallRule{
@@ -1872,6 +1874,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Destination: "192.168.10.0/16",
Protocol: "tcp",
Port: 80,
RouteID: "route4:peerA",
},
{
SourceRanges: []string{
@@ -1880,6 +1883,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Action: "accept",
Destination: "192.168.10.0/16",
Protocol: "all",
RouteID: "route4:peerA",
},
}
@@ -1888,6 +1892,9 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
// peerD is also the routing peer for route1, should contain same routes firewall rules as peerA
routesFirewallRules = account.GetPeerRoutesFirewallRules(context.Background(), "peerD", validatedPeers)
assert.Len(t, routesFirewallRules, 2)
for _, rule := range expectedRoutesFirewallRules {
rule.RouteID = "route1:peerD"
}
assert.ElementsMatch(t, orderRuleSourceRanges(routesFirewallRules), orderRuleSourceRanges(expectedRoutesFirewallRules))
// peerE is a single routing peer for route 2 and route 3
@@ -1901,6 +1908,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Destination: existingNetwork.String(),
Protocol: "tcp",
PortRange: types.RulePortRange{Start: 80, End: 350},
RouteID: "route2",
},
{
SourceRanges: []string{"0.0.0.0/0"},
@@ -1909,6 +1917,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Protocol: "all",
Domains: domain.List{"example.com"},
IsDynamic: true,
RouteID: "route3",
},
{
SourceRanges: []string{"::/0"},
@@ -1917,6 +1926,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Protocol: "all",
Domains: domain.List{"example.com"},
IsDynamic: true,
RouteID: "route3",
},
}
assert.ElementsMatch(t, orderRuleSourceRanges(routesFirewallRules), orderRuleSourceRanges(expectedRoutesFirewallRules))
@@ -2676,6 +2686,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Destination: "192.168.0.0/16",
Protocol: "all",
Port: 80,
RouteID: "resource2:peerA",
},
{
SourceRanges: []string{
@@ -2687,6 +2698,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Destination: "192.168.0.0/16",
Protocol: "all",
Port: 320,
RouteID: "resource2:peerA",
},
}
@@ -2701,6 +2713,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Port: 80,
Domains: domain.List{"example.com"},
IsDynamic: true,
RouteID: "resource4:peerA",
},
{
SourceRanges: []string{
@@ -2711,6 +2724,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Protocol: "all",
Domains: domain.List{"example.com"},
IsDynamic: true,
RouteID: "resource4:peerA",
},
}
assert.ElementsMatch(t, orderRuleSourceRanges(firewallRules), orderRuleSourceRanges(append(expectedFirewallRules, additionalFirewallRules...)))
@@ -2719,6 +2733,9 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
_, routes, sourcePeers = account.GetNetworkResourcesRoutesToSync(context.Background(), "peerD", resourcePoliciesMap, resourceRoutersMap)
firewallRules = account.GetPeerNetworkResourceFirewallRules(context.Background(), account.Peers["peerD"], validatedPeers, routes, resourcePoliciesMap)
assert.Len(t, firewallRules, 2)
for _, rule := range expectedFirewallRules {
rule.RouteID = "resource2:peerD"
}
assert.ElementsMatch(t, orderRuleSourceRanges(firewallRules), orderRuleSourceRanges(expectedFirewallRules))
assert.Len(t, sourcePeers, 3)
@@ -2736,6 +2753,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Destination: "10.10.10.0/24",
Protocol: "tcp",
PortRange: types.RulePortRange{Start: 80, End: 350},
RouteID: "resource1:peerE",
},
}
assert.ElementsMatch(t, orderRuleSourceRanges(firewallRules), orderRuleSourceRanges(expectedFirewallRules))
@@ -2758,6 +2776,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Destination: "10.12.12.1/32",
Protocol: "tcp",
Port: 8080,
RouteID: "resource5:peerL",
},
}
assert.ElementsMatch(t, orderRuleSourceRanges(firewallRules), orderRuleSourceRanges(expectedFirewallRules))
+46 -10
View File
@@ -658,6 +658,21 @@ func (s *SqlStore) GetAllAccounts(ctx context.Context) (all []*types.Account) {
return all
}
func (s *SqlStore) GetAccountMeta(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.AccountMeta, error) {
var accountMeta types.AccountMeta
result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}).
First(&accountMeta, idQueryCondition, accountID)
if result.Error != nil {
log.WithContext(ctx).Errorf("error when getting account meta %s from the store: %s", accountID, result.Error)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, status.NewAccountNotFoundError(accountID)
}
return nil, status.NewGetAccountFromStoreError(result.Error)
}
return &accountMeta, nil
}
func (s *SqlStore) GetAccount(ctx context.Context, accountID string) (*types.Account, error) {
start := time.Now()
defer func() {
@@ -785,6 +800,19 @@ func (s *SqlStore) GetAccountByPeerPubKey(ctx context.Context, peerKey string) (
return s.GetAccount(ctx, peer.AccountID)
}
func (s *SqlStore) GetAnyAccountID(ctx context.Context) (string, error) {
var account types.Account
result := s.db.WithContext(ctx).Select("id").Order("created_at desc").Limit(1).Find(&account)
if result.Error != nil {
return "", status.NewGetAccountFromStoreError(result.Error)
}
if result.RowsAffected == 0 {
return "", status.Errorf(status.NotFound, "account not found: index lookup failed")
}
return account.Id, nil
}
func (s *SqlStore) GetAccountIDByPeerPubKey(ctx context.Context, peerKey string) (string, error) {
var peer nbpeer.Peer
var accountID string
@@ -1655,18 +1683,26 @@ func (s *SqlStore) SavePolicy(ctx context.Context, lockStrength LockingStrength,
}
func (s *SqlStore) DeletePolicy(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) error {
result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
Delete(&types.Policy{}, accountAndIDQueryCondition, accountID, policyID)
if err := result.Error; err != nil {
log.WithContext(ctx).Errorf("failed to delete policy from store: %s", err)
return status.Errorf(status.Internal, "failed to delete policy from store")
}
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("policy_id = ?", policyID).Delete(&types.PolicyRule{}).Error; err != nil {
return fmt.Errorf("delete policy rules: %w", err)
}
if result.RowsAffected == 0 {
return status.NewPolicyNotFoundError(policyID)
}
result := tx.Clauses(clause.Locking{Strength: string(lockStrength)}).
Where(accountAndIDQueryCondition, accountID, policyID).
Delete(&types.Policy{})
return nil
if err := result.Error; err != nil {
log.WithContext(ctx).Errorf("failed to delete policy from store: %s", err)
return status.Errorf(status.Internal, "failed to delete policy from store")
}
if result.RowsAffected == 0 {
return status.NewPolicyNotFoundError(policyID)
}
return nil
})
}
// GetAccountPostureChecks retrieves posture checks for an account.
+44 -3
View File
@@ -60,10 +60,10 @@ func Test_NewStore(t *testing.T) {
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
if store == nil {
t.Errorf("expected to create a new Store")
t.Fatalf("expected to create a new Store")
}
if len(store.GetAllAccounts(context.Background())) != 0 {
t.Errorf("expected to create a new empty Accounts map when creating a new FileStore")
t.Fatalf("expected to create a new empty Accounts map when creating a new FileStore")
}
})
}
@@ -1115,7 +1115,7 @@ func TestSqlite_CreateAndGetObjectInTransaction(t *testing.T) {
group := &types.Group{
ID: "group-id",
AccountID: "account-id",
AccountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b",
Name: "group-name",
Issued: "api",
Peers: nil,
@@ -3247,3 +3247,44 @@ func TestSqlStore_SaveGroups_LargeBatch(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 8003, len(accountGroups))
}
func TestSqlStore_GetAccountMeta(t *testing.T) {
store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
t.Cleanup(cleanup)
require.NoError(t, err)
accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
accountMeta, err := store.GetAccountMeta(context.Background(), LockingStrengthShare, accountID)
require.NoError(t, err)
require.NotNil(t, accountMeta)
require.Equal(t, accountID, accountMeta.AccountID)
require.Equal(t, "edafee4e-63fb-11ec-90d6-0242ac120003", accountMeta.CreatedBy)
require.Equal(t, "test.com", accountMeta.Domain)
require.Equal(t, "private", accountMeta.DomainCategory)
require.Equal(t, time.Date(2024, time.October, 2, 14, 1, 38, 210000000, time.UTC), accountMeta.CreatedAt.UTC())
}
func TestSqlStore_GetAnyAccountID(t *testing.T) {
t.Run("should return account ID when accounts exist", func(t *testing.T) {
store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
t.Cleanup(cleanup)
require.NoError(t, err)
accountID, err := store.GetAnyAccountID(context.Background())
require.NoError(t, err)
assert.Equal(t, "bf1c8084-ba50-4ce7-9439-34653001fc3b", accountID)
})
t.Run("should return error when no accounts exist", func(t *testing.T) {
store, cleanup, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir())
t.Cleanup(cleanup)
require.NoError(t, err)
accountID, err := store.GetAnyAccountID(context.Background())
require.Error(t, err)
sErr, ok := status.FromError(err)
assert.True(t, ok)
assert.Equal(t, sErr.Type(), status.NotFound)
assert.Empty(t, accountID)
})
}
+11
View File
@@ -50,10 +50,12 @@ type Store interface {
GetAccountsCounter(ctx context.Context) (int64, error)
GetAllAccounts(ctx context.Context) []*types.Account
GetAccount(ctx context.Context, accountID string) (*types.Account, error)
GetAccountMeta(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.AccountMeta, error)
AccountExists(ctx context.Context, lockStrength LockingStrength, id string) (bool, error)
GetAccountDomainAndCategory(ctx context.Context, lockStrength LockingStrength, accountID string) (string, string, error)
GetAccountByUser(ctx context.Context, userID string) (*types.Account, error)
GetAccountByPeerPubKey(ctx context.Context, peerKey string) (*types.Account, error)
GetAnyAccountID(ctx context.Context) (string, error)
GetAccountIDByPeerPubKey(ctx context.Context, peerKey string) (string, error)
GetAccountIDByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (string, error)
GetAccountIDBySetupKey(ctx context.Context, peerKey string) (string, error)
@@ -313,6 +315,15 @@ func getMigrations(ctx context.Context) []migrationFunc {
func(db *gorm.DB) error {
return migration.MigrateNewField[routerTypes.NetworkRouter](ctx, db, "enabled", true)
},
func(db *gorm.DB) error {
return migration.DropIndex[networkTypes.Network](ctx, db, "idx_networks_id")
},
func(db *gorm.DB) error {
return migration.DropIndex[resourceTypes.NetworkResource](ctx, db, "idx_network_resources_id")
},
func(db *gorm.DB) error {
return migration.DropIndex[routerTypes.NetworkRouter](ctx, db, "idx_network_routers_id")
},
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ CREATE INDEX `idx_routes_account_id` ON `routes`(`account_id`);
CREATE INDEX `idx_name_server_groups_account_id` ON `name_server_groups`(`account_id`);
CREATE INDEX `idx_posture_checks_account_id` ON `posture_checks`(`account_id`);
INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','','2024-10-02 16:01:38.210014+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL);
INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','edafee4e-63fb-11ec-90d6-0242ac120003','2024-10-02 16:01:38.210000+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL);
INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBB','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cfefqs706sqkneg59g2g"]',0,0);
INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBC','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBC','Faulty key with non existing group','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["abcd"]',0,0);
INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','["cfefqs706sqkneg59g3g"]',0,NULL,'2024-10-02 16:01:38.210678+02:00','api',0,'');
+26
View File
@@ -12,6 +12,7 @@ import (
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/mysql"
"github.com/testcontainers/testcontainers-go/modules/postgres"
testcontainersredis "github.com/testcontainers/testcontainers-go/modules/redis"
"github.com/testcontainers/testcontainers-go/wait"
)
@@ -84,3 +85,28 @@ func CreatePostgresTestContainer() (func(), error) {
return cleanup, os.Setenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN", talksConn)
}
// CreateRedisTestContainer creates a new Redis container for testing.
func CreateRedisTestContainer() (func(), string, error) {
ctx := context.Background()
redisContainer, err := testcontainersredis.RunContainer(ctx, testcontainers.WithImage("redis:7"))
if err != nil {
return nil, "", err
}
cleanup := func() {
timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
defer cancelFunc()
if err = redisContainer.Terminate(timeoutCtx); err != nil {
log.WithContext(ctx).Warnf("failed to stop redis container %s: %s", redisContainer.GetContainerID(), err)
}
}
redisURL, err := redisContainer.ConnectionString(ctx)
if err != nil {
return nil, "", err
}
return cleanup, redisURL, nil
}
+6
View File
@@ -14,3 +14,9 @@ func CreateMysqlTestContainer() (func(), error) {
// Empty function for MySQL
}, nil
}
func CreateRedisTestContainer() (func(), string, error) {
return func() {
// Empty function for Redis
}, "", nil
}
+23 -1
View File
@@ -40,6 +40,17 @@ const (
type LookupMap map[string]struct{}
// AccountMeta is a struct that contains a stripped down version of the Account object.
// It doesn't carry any peers, groups, policies, or routes, etc. Just some metadata (e.g. ID, created by, created at, etc).
type AccountMeta struct {
// AccountId is the unique identifier of the account
AccountID string `gorm:"column:id"`
CreatedAt time.Time
CreatedBy string
Domain string
DomainCategory string
}
// Account represents a unique account of the system
type Account struct {
// we have to name column to aid as it collides with Network.Id when work with associations
@@ -855,6 +866,16 @@ func (a *Account) Copy() *Account {
}
}
func (a *Account) GetMeta() *AccountMeta {
return &AccountMeta{
AccountID: a.Id,
CreatedBy: a.CreatedBy,
CreatedAt: a.CreatedAt,
Domain: a.Domain,
DomainCategory: a.DomainCategory,
}
}
func (a *Account) GetGroupAll() (*Group, error) {
for _, g := range a.Groups {
if g.Name == "All" {
@@ -1219,6 +1240,7 @@ func getDefaultPermit(route *route.Route) []*RouteFirewallRule {
Protocol: string(PolicyRuleProtocolALL),
Domains: route.Domains,
IsDynamic: route.IsDynamic(),
RouteID: route.ID,
}
rules = append(rules, &rule)
@@ -1267,7 +1289,7 @@ func (a *Account) GetPeerNetworkResourceFirewallRules(ctx context.Context, peer
if route.Peer != peer.Key {
continue
}
resourceAppliedPolicies := resourcePolicies[route.GetResourceID()]
resourceAppliedPolicies := resourcePolicies[string(route.GetResourceID())]
distributionPeers := getPoliciesSourcePeers(resourceAppliedPolicies, a.Groups)
rules := a.getRouteFirewallRules(ctx, peer.ID, resourceAppliedPolicies, route, validatedPeersMap, distributionPeers)
+1
View File
@@ -62,6 +62,7 @@ func generateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule
baseRule := RouteFirewallRule{
PolicyID: rule.PolicyID,
RouteID: route.ID,
SourceRanges: sourceRanges,
Action: string(rule.Action),
Destination: route.Network.String(),
+1 -1
View File
@@ -14,7 +14,7 @@ const (
// Group of the peers for ACL
type Group struct {
// ID of the group
ID string
ID string `gorm:"primaryKey"`
// AccountID is a reference to Account that this object belongs
AccountID string `json:"-" gorm:"index"`
@@ -2,6 +2,7 @@ package types
import (
"github.com/netbirdio/netbird/management/domain"
"github.com/netbirdio/netbird/route"
)
// RouteFirewallRule a firewall rule applicable for a routed network.
@@ -9,6 +10,9 @@ type RouteFirewallRule struct {
// PolicyID is the ID of the policy this rule is derived from
PolicyID string
// RouteID is the ID of the route this rule belongs to.
RouteID route.ID
// SourceRanges IP ranges of the routing peers.
SourceRanges []string
+4
View File
@@ -39,6 +39,9 @@ type Settings struct {
// RoutingPeerDNSResolutionEnabled enabled the DNS resolution on the routing peers
RoutingPeerDNSResolutionEnabled bool
// DNSDomain is the custom domain for that account
DNSDomain string
// Extra is a dictionary of Account settings
Extra *ExtraSettings `gorm:"embedded;embeddedPrefix:extra_"`
}
@@ -58,6 +61,7 @@ func (s *Settings) Copy() *Settings {
PeerInactivityExpiration: s.PeerInactivityExpiration,
RoutingPeerDNSResolutionEnabled: s.RoutingPeerDNSResolutionEnabled,
DNSDomain: s.DNSDomain,
}
if s.Extra != nil {
settings.Extra = s.Extra.Copy()
+12 -20
View File
@@ -15,6 +15,8 @@ const (
UserRoleUser UserRole = "user"
UserRoleUnknown UserRole = "unknown"
UserRoleBillingAdmin UserRole = "billing_admin"
UserRoleAuditor UserRole = "auditor"
UserRoleNetworkAdmin UserRole = "network_admin"
UserStatusActive UserStatus = "active"
UserStatusDisabled UserStatus = "disabled"
@@ -35,6 +37,10 @@ func StrRoleToUserRole(strRole string) UserRole {
return UserRoleUser
case "billing_admin":
return UserRoleBillingAdmin
case "auditor":
return UserRoleAuditor
case "network_admin":
return UserRoleNetworkAdmin
default:
return UserRoleUnknown
}
@@ -59,11 +65,6 @@ type UserInfo struct {
LastLogin time.Time `json:"last_login"`
Issued string `json:"issued"`
IntegrationReference integration_reference.IntegrationReference `json:"-"`
Permissions UserPermissions `json:"permissions"`
}
type UserPermissions struct {
DashboardView string `json:"dashboard_view"`
}
// User represents a user of the system
@@ -126,21 +127,18 @@ func (u *User) IsRegularUser() bool {
return !u.HasAdminPower() && !u.IsServiceUser
}
// IsRestrictable checks whether a user is in a restrictable role.
func (u *User) IsRestrictable() bool {
return u.Role == UserRoleUser || u.Role == UserRoleBillingAdmin
}
// ToUserInfo converts a User object to a UserInfo object.
func (u *User) ToUserInfo(userData *idp.UserData, settings *Settings) (*UserInfo, error) {
func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) {
autoGroups := u.AutoGroups
if autoGroups == nil {
autoGroups = []string{}
}
dashboardViewPermissions := "full"
if !u.HasAdminPower() {
dashboardViewPermissions = "limited"
if settings.RegularUsersViewBlocked {
dashboardViewPermissions = "blocked"
}
}
if userData == nil {
return &UserInfo{
ID: u.Id,
@@ -153,9 +151,6 @@ func (u *User) ToUserInfo(userData *idp.UserData, settings *Settings) (*UserInfo
IsBlocked: u.Blocked,
LastLogin: u.GetLastLogin(),
Issued: u.Issued,
Permissions: UserPermissions{
DashboardView: dashboardViewPermissions,
},
}, nil
}
if userData.ID != u.Id {
@@ -178,9 +173,6 @@ func (u *User) ToUserInfo(userData *idp.UserData, settings *Settings) (*UserInfo
IsBlocked: u.Blocked,
LastLogin: u.GetLastLogin(),
Issued: u.Issued,
Permissions: UserPermissions{
DashboardView: dashboardViewPermissions,
},
}, nil
}
+35 -33
View File
@@ -12,6 +12,7 @@ import (
"github.com/netbirdio/netbird/management/server/activity"
nbContext "github.com/netbirdio/netbird/management/server/context"
nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/idp"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/permissions/modules"
@@ -19,6 +20,7 @@ import (
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/users"
"github.com/netbirdio/netbird/management/server/util"
)
@@ -122,11 +124,6 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u
CreatedAt: time.Now().UTC(),
}
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, err
}
if err = am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser); err != nil {
return nil, err
}
@@ -138,7 +135,7 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u
am.StoreEvent(ctx, userID, newUser.Id, accountID, activity.UserInvited, nil)
return newUser.ToUserInfo(idpUser, settings)
return newUser.ToUserInfo(idpUser)
}
// createNewIdpUser validates the invite and creates a new user in the IdP
@@ -360,6 +357,7 @@ func (am *DefaultAccountManager) CreatePAT(ctx context.Context, accountID string
return nil, err
}
// @note this is essential to prevent non admin users with Pats create permission frpm creating one for a service user
if initiatorUserID != targetUserID && !(initiatorUser.HasAdminPower() && targetUser.IsServiceUser) {
return nil, status.NewAdminPermissionError()
}
@@ -727,19 +725,14 @@ func handleOwnerRoleTransfer(ctx context.Context, transaction store.Store, initi
// If the AccountManager has a non-nil idpManager and the User is not a service user,
// it will attempt to look up the UserData from the cache.
func (am *DefaultAccountManager) getUserInfo(ctx context.Context, user *types.User, accountID string) (*types.UserInfo, error) {
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, err
}
if !isNil(am.idpManager) && !user.IsServiceUser {
userData, err := am.lookupUserInCache(ctx, user.Id, accountID)
if err != nil {
return nil, err
}
return user.ToUserInfo(userData, settings)
return user.ToUserInfo(userData)
}
return user.ToUserInfo(nil, settings)
return user.ToUserInfo(nil)
}
// validateUserUpdate validates the update operation for a user.
@@ -879,17 +872,12 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
queriedUsers = append(queriedUsers, usersFromIntegration...)
}
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, err
}
userInfosMap := make(map[string]*types.UserInfo)
// in case of self-hosted, or IDP doesn't return anything, we will return the locally stored userInfo
if len(queriedUsers) == 0 {
for _, accountUser := range accountUsers {
info, err := accountUser.ToUserInfo(nil, settings)
info, err := accountUser.ToUserInfo(nil)
if err != nil {
return nil, err
}
@@ -902,7 +890,7 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
for _, localUser := range accountUsers {
var info *types.UserInfo
if queriedUser, contains := findUserInIDPUserdata(localUser.Id, queriedUsers); contains {
info, err = localUser.ToUserInfo(queriedUser, settings)
info, err = localUser.ToUserInfo(queriedUser)
if err != nil {
return nil, err
}
@@ -912,14 +900,6 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
name = localUser.ServiceUserName
}
dashboardViewPermissions := "full"
if !localUser.HasAdminPower() {
dashboardViewPermissions = "limited"
if settings.RegularUsersViewBlocked {
dashboardViewPermissions = "blocked"
}
}
info = &types.UserInfo{
ID: localUser.Id,
Email: "",
@@ -929,7 +909,6 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
Status: string(types.UserStatusActive),
IsServiceUser: localUser.IsServiceUser,
NonDeletable: localUser.NonDeletable,
Permissions: types.UserPermissions{DashboardView: dashboardViewPermissions},
}
}
userInfosMap[info.ID] = info
@@ -940,6 +919,12 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
// expireAndUpdatePeers expires all peers of the given user and updates them in the account
func (am *DefaultAccountManager) expireAndUpdatePeers(ctx context.Context, accountID string, peers []*nbpeer.Peer) error {
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return err
}
dnsDomain := am.GetDNSDomain(settings)
var peerIDs []string
for _, peer := range peers {
// nolint:staticcheck
@@ -957,7 +942,7 @@ func (am *DefaultAccountManager) expireAndUpdatePeers(ctx context.Context, accou
am.StoreEvent(
ctx,
peer.UserID, peer.ID, accountID,
activity.PeerLoginExpired, peer.EventMeta(am.GetDNSDomain()),
activity.PeerLoginExpired, peer.EventMeta(dnsDomain),
)
}
@@ -1233,8 +1218,10 @@ func validateUserInvite(invite *types.UserInfo) error {
return nil
}
// GetCurrentUserInfo retrieves the account's current user info
func (am *DefaultAccountManager) GetCurrentUserInfo(ctx context.Context, accountID, userID string) (*types.UserInfo, error) {
// GetCurrentUserInfo retrieves the account's current user info and permissions
func (am *DefaultAccountManager) GetCurrentUserInfo(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error) {
accountID, userID := userAuth.AccountId, userAuth.UserId
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
if err != nil {
return nil, err
@@ -1252,10 +1239,25 @@ func (am *DefaultAccountManager) GetCurrentUserInfo(ctx context.Context, account
return nil, err
}
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, err
}
userInfo, err := am.getUserInfo(ctx, user, accountID)
if err != nil {
return nil, err
}
return userInfo, nil
userWithPermissions := &users.UserInfoWithPermissions{
UserInfo: userInfo,
Restricted: !userAuth.IsChild && user.IsRestrictable() && settings.RegularUsersViewBlocked,
}
permissions, err := am.permissionsManager.GetPermissionsByRole(ctx, user.Role)
if err == nil {
userWithPermissions.Permissions = permissions
}
return userWithPermissions, nil
}
+127 -161
View File
@@ -13,7 +13,10 @@ import (
nbcache "github.com/netbirdio/netbird/management/server/cache"
nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/permissions/modules"
"github.com/netbirdio/netbird/management/server/permissions/roles"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/users"
"github.com/netbirdio/netbird/management/server/util"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
@@ -1020,90 +1023,6 @@ func TestDefaultAccountManager_ListUsers(t *testing.T) {
assert.Equal(t, 2, regular)
}
func TestDefaultAccountManager_ListUsers_DashboardPermissions(t *testing.T) {
testCases := []struct {
name string
role types.UserRole
limitedViewSettings bool
expectedDashboardPermissions string
}{
{
name: "Regular user, no limited view settings",
role: types.UserRoleUser,
limitedViewSettings: false,
expectedDashboardPermissions: "limited",
},
{
name: "Admin user, no limited view settings",
role: types.UserRoleAdmin,
limitedViewSettings: false,
expectedDashboardPermissions: "full",
},
{
name: "Owner, no limited view settings",
role: types.UserRoleOwner,
limitedViewSettings: false,
expectedDashboardPermissions: "full",
},
{
name: "Regular user, limited view settings",
role: types.UserRoleUser,
limitedViewSettings: true,
expectedDashboardPermissions: "blocked",
},
{
name: "Admin user, limited view settings",
role: types.UserRoleAdmin,
limitedViewSettings: true,
expectedDashboardPermissions: "full",
},
{
name: "Owner, limited view settings",
role: types.UserRoleOwner,
limitedViewSettings: true,
expectedDashboardPermissions: "full",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
if err != nil {
t.Fatalf("Error when creating store: %s", err)
}
t.Cleanup(cleanup)
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "")
account.Users["normal_user1"] = types.NewUser("normal_user1", testCase.role, false, false, "", []string{}, types.UserIssuedAPI)
account.Settings.RegularUsersViewBlocked = testCase.limitedViewSettings
delete(account.Users, mockUserID)
err = store.SaveAccount(context.Background(), account)
if err != nil {
t.Fatalf("Error when saving account: %s", err)
}
permissionsManager := permissions.NewManager(store)
am := DefaultAccountManager{
Store: store,
eventStore: &activity.InMemoryEventStore{},
permissionsManager: permissionsManager,
}
users, err := am.ListUsers(context.Background(), mockAccountID)
if err != nil {
t.Fatalf("Error when checking user role: %s", err)
}
assert.Equal(t, 1, len(users))
userInfo, _ := users[0].ToUserInfo(nil, account.Settings)
assert.Equal(t, testCase.expectedDashboardPermissions, userInfo.Permissions.DashboardView)
})
}
}
func TestDefaultAccountManager_ExternalCache(t *testing.T) {
store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
if err != nil {
@@ -1654,121 +1573,154 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
tt := []struct {
name string
accountId string
userId string
userAuth nbcontext.UserAuth
expectedErr error
expectedResult *types.UserInfo
expectedResult *users.UserInfoWithPermissions
}{
{
name: "not found",
accountId: account1.Id,
userId: "not-found",
userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "not-found"},
expectedErr: status.NewUserNotFoundError("not-found"),
},
{
name: "not part of account",
accountId: account1.Id,
userId: "account2Owner",
userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "account2Owner"},
expectedErr: status.NewUserNotPartOfAccountError(),
},
{
name: "blocked",
accountId: account1.Id,
userId: "blocked-user",
userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "blocked-user"},
expectedErr: status.NewUserBlockedError(),
},
{
name: "service user",
accountId: account1.Id,
userId: "service-user",
userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "service-user"},
expectedErr: status.NewPermissionDeniedError(),
},
{
name: "owner user",
accountId: account1.Id,
userId: "account1Owner",
expectedResult: &types.UserInfo{
ID: "account1Owner",
Name: "",
Role: "owner",
AutoGroups: []string{},
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{},
Permissions: types.UserPermissions{
DashboardView: "full",
name: "owner user",
userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "account1Owner"},
expectedResult: &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "account1Owner",
Name: "",
Role: "owner",
AutoGroups: []string{},
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{},
},
Permissions: mergeRolePermissions(roles.Owner),
},
},
{
name: "regular user",
accountId: account1.Id,
userId: "regular-user",
expectedResult: &types.UserInfo{
ID: "regular-user",
Name: "",
Role: "user",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{},
Permissions: types.UserPermissions{
DashboardView: "limited",
name: "regular user",
userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "regular-user"},
expectedResult: &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "regular-user",
Name: "",
Role: "user",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{},
},
Permissions: mergeRolePermissions(roles.User),
},
},
{
name: "admin user",
accountId: account1.Id,
userId: "admin-user",
expectedResult: &types.UserInfo{
ID: "admin-user",
Name: "",
Role: "admin",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{},
Permissions: types.UserPermissions{
DashboardView: "full",
name: "admin user",
userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "admin-user"},
expectedResult: &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "admin-user",
Name: "",
Role: "admin",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{},
},
Permissions: mergeRolePermissions(roles.Admin),
},
},
{
name: "settings blocked regular user",
accountId: account2.Id,
userId: "settings-blocked-user",
expectedResult: &types.UserInfo{
ID: "settings-blocked-user",
Name: "",
Role: "user",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{},
Permissions: types.UserPermissions{
DashboardView: "blocked",
name: "settings blocked regular user",
userAuth: nbcontext.UserAuth{AccountId: account2.Id, UserId: "settings-blocked-user"},
expectedResult: &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "settings-blocked-user",
Name: "",
Role: "user",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{},
},
Permissions: mergeRolePermissions(roles.User),
Restricted: true,
},
},
{
name: "settings blocked regular user child account",
userAuth: nbcontext.UserAuth{AccountId: account2.Id, UserId: "settings-blocked-user", IsChild: true},
expectedResult: &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "settings-blocked-user",
Name: "",
Role: "user",
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{},
},
Permissions: mergeRolePermissions(roles.User),
Restricted: false,
},
},
{
name: "settings blocked owner user",
userAuth: nbcontext.UserAuth{AccountId: account2.Id, UserId: "account2Owner"},
expectedResult: &users.UserInfoWithPermissions{
UserInfo: &types.UserInfo{
ID: "account2Owner",
Name: "",
Role: "owner",
AutoGroups: []string{},
Status: "active",
IsServiceUser: false,
IsBlocked: false,
NonDeletable: false,
LastLogin: time.Time{},
Issued: "api",
IntegrationReference: integration_reference.IntegrationReference{},
},
Permissions: mergeRolePermissions(roles.Owner),
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
result, err := am.GetCurrentUserInfo(context.Background(), tc.accountId, tc.userId)
result, err := am.GetCurrentUserInfo(context.Background(), tc.userAuth)
if tc.expectedErr != nil {
assert.Equal(t, err, tc.expectedErr)
@@ -1780,3 +1732,17 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
})
}
}
func mergeRolePermissions(role roles.RolePermissions) roles.Permissions {
permissions := roles.Permissions{}
for k := range modules.All {
if rolePermissions, ok := role.Permissions[k]; ok {
permissions[k] = rolePermissions
continue
}
permissions[k] = role.AutoAllowNew
}
return permissions
}
+14
View File
@@ -0,0 +1,14 @@
package users
import (
"github.com/netbirdio/netbird/management/server/permissions/roles"
"github.com/netbirdio/netbird/management/server/types"
)
// Wrapped UserInfo with Role Permissions
type UserInfoWithPermissions struct {
*types.UserInfo
Permissions roles.Permissions
Restricted bool
}