mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 16:26:38 +00:00
Merge branch 'main' into feature/add_PAT_generation
This commit is contained in:
@@ -49,21 +49,18 @@ type AccountManager interface {
|
||||
SaveUser(accountID, userID string, update *User) (*UserInfo, error)
|
||||
GetSetupKey(accountID, userID, keyID string) (*SetupKey, error)
|
||||
GetAccountByUserOrAccountID(userID, accountID, domain string) (*Account, error)
|
||||
GetAccountByPeerID(peerID string) (*Account, error)
|
||||
GetAccountFromToken(claims jwtclaims.AuthorizationClaims) (*Account, *User, error)
|
||||
IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error)
|
||||
AccountExists(accountId string) (*bool, error)
|
||||
GetPeerByKey(peerKey string) (*Peer, error)
|
||||
GetPeers(accountID, userID string) ([]*Peer, error)
|
||||
MarkPeerConnected(peerKey string, connected bool) error
|
||||
MarkPeerLoginExpired(peerPubKey string, loginExpired bool) error
|
||||
DeletePeer(accountID, peerID, userID string) (*Peer, error)
|
||||
GetPeerByIP(accountId string, peerIP string) (*Peer, error)
|
||||
UpdatePeer(accountID, userID string, peer *Peer) (*Peer, error)
|
||||
GetNetworkMap(peerID string) (*NetworkMap, error)
|
||||
GetPeerNetwork(peerID string) (*Network, error)
|
||||
AddPeer(setupKey, userID string, peer *Peer) (*Peer, error)
|
||||
UpdatePeerMeta(peerID string, meta PeerSystemMeta) error
|
||||
AddPeer(setupKey, userID string, peer *Peer) (*Peer, *NetworkMap, error)
|
||||
UpdatePeerSSHKey(peerID string, sshKey string) error
|
||||
GetUsersFromAccount(accountID, userID string) ([]*UserInfo, error)
|
||||
GetGroup(accountId, groupID string) (*Group, error)
|
||||
@@ -74,11 +71,10 @@ type AccountManager interface {
|
||||
GroupAddPeer(accountId, groupID, peerID string) error
|
||||
GroupDeletePeer(accountId, groupID, peerKey string) error
|
||||
GroupListPeers(accountId, groupID string) ([]*Peer, error)
|
||||
GetRule(accountID, ruleID, userID string) (*Rule, error)
|
||||
SaveRule(accountID, userID string, rule *Rule) error
|
||||
UpdateRule(accountID string, ruleID string, operations []RuleUpdateOperation) (*Rule, error)
|
||||
DeleteRule(accountID, ruleID, userID string) error
|
||||
ListRules(accountID, userID string) ([]*Rule, error)
|
||||
GetPolicy(accountID, policyID, userID string) (*Policy, error)
|
||||
SavePolicy(accountID, userID string, policy *Policy) error
|
||||
DeletePolicy(accountID, policyID, userID string) error
|
||||
ListPolicies(accountID, userID string) ([]*Policy, error)
|
||||
GetRoute(accountID, routeID, userID string) (*route.Route, error)
|
||||
CreateRoute(accountID string, prefix, peerID, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error)
|
||||
SaveRoute(accountID, userID string, route *route.Route) error
|
||||
@@ -96,8 +92,9 @@ type AccountManager interface {
|
||||
GetDNSSettings(accountID string, userID string) (*DNSSettings, error)
|
||||
SaveDNSSettings(accountID string, userID string, dnsSettingsToSave *DNSSettings) error
|
||||
GetPeer(accountID, peerID, userID string) (*Peer, error)
|
||||
UpdatePeerLastLogin(peerID string) error
|
||||
UpdateAccountSettings(accountID, userID string, newSettings *Settings) (*Account, error)
|
||||
LoginPeer(login PeerLogin) (*Peer, *NetworkMap, error) // used by peer gRPC API
|
||||
SyncPeer(sync PeerSync) (*Peer, *NetworkMap, error) // used by peer gRPC API
|
||||
}
|
||||
|
||||
type DefaultAccountManager struct {
|
||||
@@ -154,6 +151,7 @@ type Account struct {
|
||||
Users map[string]*User
|
||||
Groups map[string]*Group
|
||||
Rules map[string]*Rule
|
||||
Policies []*Policy
|
||||
Routes map[string]*route.Route
|
||||
NameServerGroups map[string]*nbdns.NameServerGroup
|
||||
DNSSettings *DNSSettings
|
||||
@@ -267,47 +265,52 @@ func (a *Account) GetPeerByIP(peerIP string) *Peer {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPeerRules returns a list of source or destination rules of a given peer.
|
||||
func (a *Account) GetPeerRules(peerID string) (srcRules []*Rule, dstRules []*Rule) {
|
||||
// Rules are group based so there is no direct access to peers.
|
||||
// First, find all groups that the given peer belongs to
|
||||
peerGroups := make(map[string]struct{})
|
||||
|
||||
for s, group := range a.Groups {
|
||||
for _, peer := range group.Peers {
|
||||
if peerID == peer {
|
||||
peerGroups[s] = struct{}{}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second, find all rules that have discovered source and destination groups
|
||||
srcRulesMap := make(map[string]*Rule)
|
||||
dstRulesMap := make(map[string]*Rule)
|
||||
for _, rule := range a.Rules {
|
||||
for _, g := range rule.Source {
|
||||
if _, ok := peerGroups[g]; ok && srcRulesMap[rule.ID] == nil {
|
||||
srcRules = append(srcRules, rule)
|
||||
srcRulesMap[rule.ID] = rule
|
||||
}
|
||||
}
|
||||
for _, g := range rule.Destination {
|
||||
if _, ok := peerGroups[g]; ok && dstRulesMap[rule.ID] == nil {
|
||||
dstRules = append(dstRules, rule)
|
||||
dstRulesMap[rule.ID] = rule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return srcRules, dstRules
|
||||
}
|
||||
|
||||
// GetGroup returns a group by ID if exists, nil otherwise
|
||||
func (a *Account) GetGroup(groupID string) *Group {
|
||||
return a.Groups[groupID]
|
||||
}
|
||||
|
||||
// GetPeerNetworkMap returns a group by ID if exists, nil otherwise
|
||||
func (a *Account) GetPeerNetworkMap(peerID, dnsDomain string) *NetworkMap {
|
||||
aclPeers, _ := a.getPeersByPolicy(peerID)
|
||||
// exclude expired peers
|
||||
var peersToConnect []*Peer
|
||||
var expiredPeers []*Peer
|
||||
for _, p := range aclPeers {
|
||||
expired, _ := p.LoginExpired(a.Settings.PeerLoginExpiration)
|
||||
if a.Settings.PeerLoginExpirationEnabled && expired {
|
||||
expiredPeers = append(expiredPeers, p)
|
||||
continue
|
||||
}
|
||||
peersToConnect = append(peersToConnect, p)
|
||||
}
|
||||
// Please mind, that the returned route.Route objects will contain Peer.Key instead of Peer.ID.
|
||||
routesUpdate := a.getRoutesToSync(peerID, peersToConnect)
|
||||
|
||||
dnsManagementStatus := a.getPeerDNSManagementStatus(peerID)
|
||||
dnsUpdate := nbdns.Config{
|
||||
ServiceEnable: dnsManagementStatus,
|
||||
}
|
||||
|
||||
if dnsManagementStatus {
|
||||
var zones []nbdns.CustomZone
|
||||
peersCustomZone := getPeersCustomZone(a, dnsDomain)
|
||||
if peersCustomZone.Domain != "" {
|
||||
zones = append(zones, peersCustomZone)
|
||||
}
|
||||
dnsUpdate.CustomZones = zones
|
||||
dnsUpdate.NameServerGroups = getPeerNSGroups(a, peerID)
|
||||
}
|
||||
|
||||
return &NetworkMap{
|
||||
Peers: peersToConnect,
|
||||
Network: a.Network.Copy(),
|
||||
Routes: routesUpdate,
|
||||
DNSConfig: dnsUpdate,
|
||||
OfflinePeers: expiredPeers,
|
||||
}
|
||||
}
|
||||
|
||||
// GetExpiredPeers returns peers that have been expired
|
||||
func (a *Account) GetExpiredPeers() []*Peer {
|
||||
var peers []*Peer
|
||||
@@ -325,7 +328,6 @@ func (a *Account) GetExpiredPeers() []*Peer {
|
||||
// If there is no peer that expires this function returns false and a duration of 0.
|
||||
// This function only considers peers that haven't been expired yet and that are connected.
|
||||
func (a *Account) GetNextPeerExpiration() (time.Duration, bool) {
|
||||
|
||||
peersWithExpiry := a.GetPeersWithExpiration()
|
||||
if len(peersWithExpiry) == 0 {
|
||||
return 0, false
|
||||
@@ -417,7 +419,6 @@ func (a *Account) FindPeerByPubKey(peerPubKey string) (*Peer, error) {
|
||||
|
||||
// FindUserPeers returns a list of peers that user owns (created)
|
||||
func (a *Account) FindUserPeers(userID string) ([]*Peer, error) {
|
||||
|
||||
peers := make([]*Peer, 0)
|
||||
for _, peer := range a.Peers {
|
||||
if peer.UserID == userID {
|
||||
@@ -537,6 +538,11 @@ func (a *Account) Copy() *Account {
|
||||
rules[id] = rule.Copy()
|
||||
}
|
||||
|
||||
policies := []*Policy{}
|
||||
for _, policy := range a.Policies {
|
||||
policies = append(policies, policy.Copy())
|
||||
}
|
||||
|
||||
routes := map[string]*route.Route{}
|
||||
for id, route := range a.Routes {
|
||||
routes[id] = route.Copy()
|
||||
@@ -569,6 +575,7 @@ func (a *Account) Copy() *Account {
|
||||
Users: users,
|
||||
Groups: groups,
|
||||
Rules: rules,
|
||||
Policies: policies,
|
||||
Routes: routes,
|
||||
NameServerGroups: nsGroups,
|
||||
DNSSettings: dnsSettings,
|
||||
@@ -626,7 +633,9 @@ func BuildManager(store Store, peersUpdateManager *PeersUpdateManager, idpManage
|
||||
|
||||
_, err := account.GetGroupAll()
|
||||
if err != nil {
|
||||
addAllGroup(account)
|
||||
if err := addAllGroup(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shouldSave = true
|
||||
}
|
||||
|
||||
@@ -662,7 +671,6 @@ func BuildManager(store Store, peersUpdateManager *PeersUpdateManager, idpManage
|
||||
// User that performs the update has to belong to the account.
|
||||
// Returns an updated Account
|
||||
func (am *DefaultAccountManager) UpdateAccountSettings(accountID, userID string, newSettings *Settings) (*Account, error) {
|
||||
|
||||
halfYearLimit := 180 * 24 * time.Hour
|
||||
if newSettings.PeerLoginExpiration > halfYearLimit {
|
||||
return nil, status.Errorf(status.InvalidArgument, "peer login expiration can't be larger than 180 days")
|
||||
@@ -747,7 +755,7 @@ func (am *DefaultAccountManager) peerLoginExpirationJob(accountID string) func()
|
||||
if len(peerIDs) != 0 {
|
||||
// this will trigger peer disconnect from the management service
|
||||
am.peersUpdateManager.CloseChannels(peerIDs)
|
||||
err := am.updateAccountPeers(account)
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
log.Errorf("failed updating account peers while expiring peers for account %s", accountID)
|
||||
return account.GetNextPeerExpiration()
|
||||
@@ -803,11 +811,6 @@ func (am *DefaultAccountManager) warmupIDPCache() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAccountByPeerID returns account from the store by a provided peer ID
|
||||
func (am *DefaultAccountManager) GetAccountByPeerID(peerID string) (*Account, error) {
|
||||
return am.Store.GetAccountByPeerID(peerID)
|
||||
}
|
||||
|
||||
// GetAccountByUserOrAccountID looks for an account by user or accountID, if no account is provided and
|
||||
// userID doesn't have an account associated with it, one account is created
|
||||
func (am *DefaultAccountManager) GetAccountByUserOrAccountID(userID, accountID, domain string) (*Account, error) {
|
||||
@@ -850,10 +853,6 @@ func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(userID string, account
|
||||
}
|
||||
|
||||
err = am.idpManager.UpdateUserAppMetadata(userID, idp.AppMetadata{WTAccountID: account.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return status.Errorf(status.Internal, "updating user's app metadata failed with: %v", err)
|
||||
}
|
||||
@@ -1242,7 +1241,7 @@ func (am *DefaultAccountManager) GetDNSDomain() string {
|
||||
}
|
||||
|
||||
// addAllGroup to account object if it doesn't exists
|
||||
func addAllGroup(account *Account) {
|
||||
func addAllGroup(account *Account) error {
|
||||
if len(account.Groups) == 0 {
|
||||
allGroup := &Group{
|
||||
ID: xid.New().String(),
|
||||
@@ -1262,7 +1261,15 @@ func addAllGroup(account *Account) {
|
||||
Destination: []string{allGroup.ID},
|
||||
}
|
||||
account.Rules = map[string]*Rule{defaultRule.ID: defaultRule}
|
||||
|
||||
// TODO: after migration we need to drop rule and create policy directly
|
||||
defaultPolicy, err := RuleToPolicy(defaultRule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert rule to policy: %w", err)
|
||||
}
|
||||
account.Policies = []*Policy{defaultPolicy}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newAccountWithId creates a new Account with a default SetupKey (doesn't store in a Store) and provided id
|
||||
@@ -1303,7 +1310,9 @@ func newAccountWithId(accountId, userId, domain string) *Account {
|
||||
},
|
||||
}
|
||||
|
||||
addAllGroup(acc)
|
||||
if err := addAllGroup(acc); err != nil {
|
||||
log.Errorf("error adding all group to account %s: %v", acc.Id, err)
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
@@ -40,7 +42,7 @@ func verifyCanAddPeerToAccount(t *testing.T, manager AccountManager, account *Ac
|
||||
setupKey = key.Key
|
||||
}
|
||||
|
||||
_, err := manager.AddPeer(setupKey, userID, peer)
|
||||
_, _, err := manager.AddPeer(setupKey, userID, peer)
|
||||
if err != nil {
|
||||
t.Error("expected to add new peer successfully after creating new account, but failed", err)
|
||||
}
|
||||
@@ -100,8 +102,123 @@ func verifyNewAccountHasDefaultFields(t *testing.T, account *Account, createdBy
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAccount(t *testing.T) {
|
||||
func TestAccount_GetPeerNetworkMap(t *testing.T) {
|
||||
peerID1 := "peer-1"
|
||||
peerID2 := "peer-2"
|
||||
tt := []struct {
|
||||
name string
|
||||
accountSettings Settings
|
||||
peerID string
|
||||
expectedPeers []string
|
||||
expectedOfflinePeers []string
|
||||
peers map[string]*Peer
|
||||
}{
|
||||
{
|
||||
name: "Should return ALL peers when global peer login expiration disabled",
|
||||
accountSettings: Settings{PeerLoginExpirationEnabled: false, PeerLoginExpiration: time.Hour},
|
||||
peerID: peerID1,
|
||||
expectedPeers: []string{peerID2},
|
||||
expectedOfflinePeers: []string{},
|
||||
peers: map[string]*Peer{
|
||||
"peer-1": {
|
||||
ID: peerID1,
|
||||
Key: "peer-1-key",
|
||||
IP: net.IP{100, 64, 0, 1},
|
||||
Name: peerID1,
|
||||
DNSLabel: peerID1,
|
||||
Status: &PeerStatus{
|
||||
LastSeen: time.Now(),
|
||||
Connected: false,
|
||||
LoginExpired: true,
|
||||
},
|
||||
UserID: userID,
|
||||
LastLogin: time.Now().Add(-time.Hour * 24 * 30 * 30),
|
||||
},
|
||||
"peer-2": {
|
||||
ID: peerID2,
|
||||
Key: "peer-2-key",
|
||||
IP: net.IP{100, 64, 0, 1},
|
||||
Name: peerID2,
|
||||
DNSLabel: peerID2,
|
||||
Status: &PeerStatus{
|
||||
LastSeen: time.Now(),
|
||||
Connected: false,
|
||||
LoginExpired: false,
|
||||
},
|
||||
UserID: userID,
|
||||
LastLogin: time.Now(),
|
||||
LoginExpirationEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Should return no peers when global peer login expiration enabled and peers expired",
|
||||
accountSettings: Settings{PeerLoginExpirationEnabled: true, PeerLoginExpiration: time.Hour},
|
||||
peerID: peerID1,
|
||||
expectedPeers: []string{},
|
||||
expectedOfflinePeers: []string{peerID2},
|
||||
peers: map[string]*Peer{
|
||||
"peer-1": {
|
||||
ID: peerID1,
|
||||
Key: "peer-1-key",
|
||||
IP: net.IP{100, 64, 0, 1},
|
||||
Name: peerID1,
|
||||
DNSLabel: peerID1,
|
||||
Status: &PeerStatus{
|
||||
LastSeen: time.Now(),
|
||||
Connected: false,
|
||||
LoginExpired: true,
|
||||
},
|
||||
UserID: userID,
|
||||
LastLogin: time.Now().Add(-time.Hour * 24 * 30 * 30),
|
||||
LoginExpirationEnabled: true,
|
||||
},
|
||||
"peer-2": {
|
||||
ID: peerID2,
|
||||
Key: "peer-2-key",
|
||||
IP: net.IP{100, 64, 0, 1},
|
||||
Name: peerID2,
|
||||
DNSLabel: peerID2,
|
||||
Status: &PeerStatus{
|
||||
LastSeen: time.Now(),
|
||||
Connected: false,
|
||||
LoginExpired: true,
|
||||
},
|
||||
UserID: userID,
|
||||
LastLogin: time.Now().Add(-time.Hour * 24 * 30 * 30),
|
||||
LoginExpirationEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
netIP := net.IP{100, 64, 0, 0}
|
||||
netMask := net.IPMask{255, 255, 0, 0}
|
||||
network := &Network{
|
||||
Id: "network",
|
||||
Net: net.IPNet{IP: netIP, Mask: netMask},
|
||||
Dns: "netbird.selfhosted",
|
||||
Serial: 0,
|
||||
mu: sync.Mutex{},
|
||||
}
|
||||
|
||||
for _, testCase := range tt {
|
||||
account := newAccountWithId("account-1", userID, "netbird.io")
|
||||
account.Network = network
|
||||
account.Peers = testCase.peers
|
||||
for _, peer := range account.Peers {
|
||||
all, _ := account.GetGroupAll()
|
||||
account.Groups[all.ID].Peers = append(account.Groups[all.ID].Peers, peer.ID)
|
||||
}
|
||||
|
||||
networkMap := account.GetPeerNetworkMap(testCase.peerID, "netbird.io")
|
||||
assert.Len(t, networkMap.Peers, len(testCase.expectedPeers))
|
||||
assert.Len(t, networkMap.OfflinePeers, len(testCase.expectedOfflinePeers))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNewAccount(t *testing.T) {
|
||||
domain := "netbird.io"
|
||||
userId := "account_creator"
|
||||
accountID := "account_id"
|
||||
@@ -544,10 +661,9 @@ func TestAccountManager_AddPeer(t *testing.T) {
|
||||
expectedPeerKey := key.PublicKey().String()
|
||||
expectedSetupKey := setupKey.Key
|
||||
|
||||
peer, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
peer, _, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
Key: expectedPeerKey,
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: expectedPeerKey,
|
||||
Meta: PeerSystemMeta{Hostname: expectedPeerKey},
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("expecting peer to be added, got failure %v", err)
|
||||
@@ -613,10 +729,9 @@ func TestAccountManager_AddPeerWithUserID(t *testing.T) {
|
||||
expectedPeerKey := key.PublicKey().String()
|
||||
expectedUserID := userID
|
||||
|
||||
peer, err := manager.AddPeer("", userID, &Peer{
|
||||
peer, _, err := manager.AddPeer("", userID, &Peer{
|
||||
Key: expectedPeerKey,
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: expectedPeerKey,
|
||||
Meta: PeerSystemMeta{Hostname: expectedPeerKey},
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("expecting peer to be added, got failure %v, account users: %v", err, account.CreatedBy)
|
||||
@@ -696,10 +811,9 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
|
||||
}
|
||||
expectedPeerKey := key.PublicKey().String()
|
||||
|
||||
peer, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
peer, _, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
Key: expectedPeerKey,
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: expectedPeerKey,
|
||||
Meta: PeerSystemMeta{Hostname: expectedPeerKey},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expecting peer1 to be added, got failure %v", err)
|
||||
@@ -728,10 +842,20 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
|
||||
Peers: []string{peer1.ID, peer2.ID, peer3.ID},
|
||||
}
|
||||
|
||||
rule := Rule{
|
||||
Source: []string{"group-id"},
|
||||
Destination: []string{"group-id"},
|
||||
Flow: TrafficFlowBidirect,
|
||||
policy := Policy{
|
||||
Enabled: true,
|
||||
Rules: []*PolicyRule{
|
||||
{
|
||||
Enabled: true,
|
||||
Sources: []string{"group-id"},
|
||||
Destinations: []string{"group-id"},
|
||||
Action: PolicyTrafficActionAccept,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := policy.UpdateQueryFromRules(); err != nil {
|
||||
t.Errorf("update policy query from rules: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
@@ -755,7 +879,7 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("delete rule update", func(t *testing.T) {
|
||||
t.Run("delete policy update", func(t *testing.T) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -767,12 +891,7 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
var defaultRule *Rule
|
||||
for _, r := range account.Rules {
|
||||
defaultRule = r
|
||||
}
|
||||
|
||||
if err := manager.DeleteRule(account.Id, defaultRule.ID, userID); err != nil {
|
||||
if err := manager.DeletePolicy(account.Id, account.Policies[0].ID, userID); err != nil {
|
||||
t.Errorf("delete default rule: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -780,7 +899,7 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("save rule update", func(t *testing.T) {
|
||||
t.Run("save policy update", func(t *testing.T) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -792,7 +911,7 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
if err := manager.SaveRule(account.Id, userID, &rule); err != nil {
|
||||
if err := manager.SavePolicy(account.Id, userID, &policy); err != nil {
|
||||
t.Errorf("delete default rule: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -833,7 +952,7 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
|
||||
}()
|
||||
|
||||
if err := manager.DeleteGroup(account.Id, group.ID); err != nil {
|
||||
t.Errorf("delete group rule: %v", err)
|
||||
t.Errorf("delete group: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -866,10 +985,9 @@ func TestAccountManager_DeletePeer(t *testing.T) {
|
||||
|
||||
peerKey := key.PublicKey().String()
|
||||
|
||||
peer, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
peer, _, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
Key: peerKey,
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: peerKey,
|
||||
Meta: PeerSystemMeta{Hostname: peerKey},
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("expecting peer to be added, got failure %v", err)
|
||||
@@ -901,6 +1019,7 @@ func TestAccountManager_DeletePeer(t *testing.T) {
|
||||
assert.Equal(t, peer.IP.String(), ev.TargetID)
|
||||
assert.Equal(t, peer.IP.String(), fmt.Sprint(ev.Meta["ip"]))
|
||||
}
|
||||
|
||||
func getEvent(t *testing.T, accountID string, manager AccountManager, eventType activity.Activity) *activity.Event {
|
||||
for {
|
||||
select {
|
||||
@@ -953,146 +1072,6 @@ func TestGetUsersFromAccount(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountManager_UpdatePeerMeta(t *testing.T) {
|
||||
manager, err := createManager(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
account, err := createAccount(manager, "test_account", "account_creator", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var setupKey *SetupKey
|
||||
for _, key := range account.SetupKeys {
|
||||
setupKey = key
|
||||
}
|
||||
|
||||
key, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
peer, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
Key: key.PublicKey().String(),
|
||||
Meta: PeerSystemMeta{
|
||||
Hostname: "Hostname",
|
||||
GoOS: "GoOS",
|
||||
Kernel: "Kernel",
|
||||
Core: "Core",
|
||||
Platform: "Platform",
|
||||
OS: "OS",
|
||||
WtVersion: "WtVersion",
|
||||
},
|
||||
Name: key.PublicKey().String(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("expecting peer to be added, got failure %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
newMeta := PeerSystemMeta{
|
||||
Hostname: "new-Hostname",
|
||||
GoOS: "new-GoOS",
|
||||
Kernel: "new-Kernel",
|
||||
Core: "new-Core",
|
||||
Platform: "new-Platform",
|
||||
OS: "new-OS",
|
||||
WtVersion: "new-WtVersion",
|
||||
}
|
||||
err = manager.UpdatePeerMeta(peer.ID, newMeta)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
p, err := manager.GetPeerByKey(peer.Key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, newMeta, p.Meta)
|
||||
}
|
||||
|
||||
func TestAccount_GetPeerRules(t *testing.T) {
|
||||
|
||||
groups := map[string]*Group{
|
||||
"group_1": {
|
||||
ID: "group_1",
|
||||
Name: "group_1",
|
||||
Peers: []string{"peer-1", "peer-2"},
|
||||
},
|
||||
"group_2": {
|
||||
ID: "group_2",
|
||||
Name: "group_2",
|
||||
Peers: []string{"peer-2", "peer-3"},
|
||||
},
|
||||
"group_3": {
|
||||
ID: "group_3",
|
||||
Name: "group_3",
|
||||
Peers: []string{"peer-4"},
|
||||
},
|
||||
"group_4": {
|
||||
ID: "group_4",
|
||||
Name: "group_4",
|
||||
Peers: []string{"peer-1"},
|
||||
},
|
||||
"group_5": {
|
||||
ID: "group_5",
|
||||
Name: "group_5",
|
||||
Peers: []string{"peer-1"},
|
||||
},
|
||||
}
|
||||
rules := map[string]*Rule{
|
||||
"rule-1": {
|
||||
ID: "rule-1",
|
||||
Name: "rule-1",
|
||||
Description: "rule-1",
|
||||
Disabled: false,
|
||||
Source: []string{"group_1", "group_5"},
|
||||
Destination: []string{"group_2"},
|
||||
Flow: 0,
|
||||
},
|
||||
"rule-2": {
|
||||
ID: "rule-2",
|
||||
Name: "rule-2",
|
||||
Description: "rule-2",
|
||||
Disabled: false,
|
||||
Source: []string{"group_1"},
|
||||
Destination: []string{"group_1"},
|
||||
Flow: 0,
|
||||
},
|
||||
"rule-3": {
|
||||
ID: "rule-3",
|
||||
Name: "rule-3",
|
||||
Description: "rule-3",
|
||||
Disabled: false,
|
||||
Source: []string{"group_3"},
|
||||
Destination: []string{"group_3"},
|
||||
Flow: 0,
|
||||
},
|
||||
}
|
||||
|
||||
account := &Account{
|
||||
Groups: groups,
|
||||
Rules: rules,
|
||||
}
|
||||
|
||||
srcRules, dstRules := account.GetPeerRules("peer-1")
|
||||
|
||||
assert.Equal(t, 2, len(srcRules))
|
||||
assert.Equal(t, 1, len(dstRules))
|
||||
|
||||
}
|
||||
|
||||
func TestFileStore_GetRoutesByPrefix(t *testing.T) {
|
||||
_, prefix, err := route.ParseNetwork("192.168.64.0/24")
|
||||
if err != nil {
|
||||
@@ -1254,6 +1233,12 @@ func TestAccount_Copy(t *testing.T) {
|
||||
ID: "rule1",
|
||||
},
|
||||
},
|
||||
Policies: []*Policy{
|
||||
{
|
||||
ID: "policy1",
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
Routes: map[string]*route.Route{
|
||||
"route1": {
|
||||
ID: "route1",
|
||||
@@ -1296,6 +1281,7 @@ func hasNilField(x interface{}) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDefaultAccountManager_DefaultAccountSettings(t *testing.T) {
|
||||
manager, err := createManager(t)
|
||||
require.NoError(t, err, "unable to create account manager")
|
||||
@@ -1307,6 +1293,7 @@ func TestDefaultAccountManager_DefaultAccountSettings(t *testing.T) {
|
||||
assert.Equal(t, account.Settings.PeerLoginExpirationEnabled, true)
|
||||
assert.Equal(t, account.Settings.PeerLoginExpiration, 24*time.Hour)
|
||||
}
|
||||
|
||||
func TestDefaultAccountManager_UpdatePeer_PeerLoginExpiration(t *testing.T) {
|
||||
manager, err := createManager(t)
|
||||
require.NoError(t, err, "unable to create account manager")
|
||||
@@ -1315,10 +1302,9 @@ func TestDefaultAccountManager_UpdatePeer_PeerLoginExpiration(t *testing.T) {
|
||||
|
||||
key, err := wgtypes.GenerateKey()
|
||||
require.NoError(t, err, "unable to generate WireGuard key")
|
||||
peer, err := manager.AddPeer("", userID, &Peer{
|
||||
peer, _, err := manager.AddPeer("", userID, &Peer{
|
||||
Key: key.PublicKey().String(),
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: "test-peer",
|
||||
Meta: PeerSystemMeta{Hostname: "test-peer"},
|
||||
LoginExpirationEnabled: true,
|
||||
})
|
||||
require.NoError(t, err, "unable to add peer")
|
||||
@@ -1326,7 +1312,8 @@ func TestDefaultAccountManager_UpdatePeer_PeerLoginExpiration(t *testing.T) {
|
||||
require.NoError(t, err, "unable to mark peer connected")
|
||||
account, err = manager.UpdateAccountSettings(account.Id, userID, &Settings{
|
||||
PeerLoginExpiration: time.Hour,
|
||||
PeerLoginExpirationEnabled: true})
|
||||
PeerLoginExpirationEnabled: true,
|
||||
})
|
||||
require.NoError(t, err, "expecting to update account settings successfully but got error")
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
@@ -1364,16 +1351,16 @@ func TestDefaultAccountManager_MarkPeerConnected_PeerLoginExpiration(t *testing.
|
||||
|
||||
key, err := wgtypes.GenerateKey()
|
||||
require.NoError(t, err, "unable to generate WireGuard key")
|
||||
_, err = manager.AddPeer("", userID, &Peer{
|
||||
_, _, err = manager.AddPeer("", userID, &Peer{
|
||||
Key: key.PublicKey().String(),
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: "test-peer",
|
||||
Meta: PeerSystemMeta{Hostname: "test-peer"},
|
||||
LoginExpirationEnabled: true,
|
||||
})
|
||||
require.NoError(t, err, "unable to add peer")
|
||||
_, err = manager.UpdateAccountSettings(account.Id, userID, &Settings{
|
||||
PeerLoginExpiration: time.Hour,
|
||||
PeerLoginExpirationEnabled: true})
|
||||
PeerLoginExpirationEnabled: true,
|
||||
})
|
||||
require.NoError(t, err, "expecting to update account settings successfully but got error")
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
@@ -1395,7 +1382,6 @@ func TestDefaultAccountManager_MarkPeerConnected_PeerLoginExpiration(t *testing.
|
||||
if failed {
|
||||
t.Fatal("timeout while waiting for test to finish")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDefaultAccountManager_UpdateAccountSettings_PeerLoginExpiration(t *testing.T) {
|
||||
@@ -1406,10 +1392,9 @@ func TestDefaultAccountManager_UpdateAccountSettings_PeerLoginExpiration(t *test
|
||||
|
||||
key, err := wgtypes.GenerateKey()
|
||||
require.NoError(t, err, "unable to generate WireGuard key")
|
||||
_, err = manager.AddPeer("", userID, &Peer{
|
||||
_, _, err = manager.AddPeer("", userID, &Peer{
|
||||
Key: key.PublicKey().String(),
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: "test-peer",
|
||||
Meta: PeerSystemMeta{Hostname: "test-peer"},
|
||||
LoginExpirationEnabled: true,
|
||||
})
|
||||
require.NoError(t, err, "unable to add peer")
|
||||
@@ -1429,7 +1414,8 @@ func TestDefaultAccountManager_UpdateAccountSettings_PeerLoginExpiration(t *test
|
||||
// enabling PeerLoginExpirationEnabled should trigger the expiration job
|
||||
account, err = manager.UpdateAccountSettings(account.Id, userID, &Settings{
|
||||
PeerLoginExpiration: time.Hour,
|
||||
PeerLoginExpirationEnabled: true})
|
||||
PeerLoginExpirationEnabled: true,
|
||||
})
|
||||
require.NoError(t, err, "expecting to update account settings successfully but got error")
|
||||
|
||||
failed := waitTimeout(wg, time.Second)
|
||||
@@ -1441,7 +1427,8 @@ func TestDefaultAccountManager_UpdateAccountSettings_PeerLoginExpiration(t *test
|
||||
// disabling PeerLoginExpirationEnabled should trigger cancel
|
||||
_, err = manager.UpdateAccountSettings(account.Id, userID, &Settings{
|
||||
PeerLoginExpiration: time.Hour,
|
||||
PeerLoginExpirationEnabled: false})
|
||||
PeerLoginExpirationEnabled: false,
|
||||
})
|
||||
require.NoError(t, err, "expecting to update account settings successfully but got error")
|
||||
failed = waitTimeout(wg, time.Second)
|
||||
if failed {
|
||||
@@ -1458,7 +1445,8 @@ func TestDefaultAccountManager_UpdateAccountSettings(t *testing.T) {
|
||||
|
||||
updated, err := manager.UpdateAccountSettings(account.Id, userID, &Settings{
|
||||
PeerLoginExpiration: time.Hour,
|
||||
PeerLoginExpirationEnabled: false})
|
||||
PeerLoginExpirationEnabled: false,
|
||||
})
|
||||
require.NoError(t, err, "expecting to update account settings successfully but got error")
|
||||
assert.False(t, updated.Settings.PeerLoginExpirationEnabled)
|
||||
assert.Equal(t, updated.Settings.PeerLoginExpiration, time.Hour)
|
||||
@@ -1471,12 +1459,14 @@ func TestDefaultAccountManager_UpdateAccountSettings(t *testing.T) {
|
||||
|
||||
_, err = manager.UpdateAccountSettings(account.Id, userID, &Settings{
|
||||
PeerLoginExpiration: time.Second,
|
||||
PeerLoginExpirationEnabled: false})
|
||||
PeerLoginExpirationEnabled: false,
|
||||
})
|
||||
require.Error(t, err, "expecting to fail when providing PeerLoginExpiration less than one hour")
|
||||
|
||||
_, err = manager.UpdateAccountSettings(account.Id, userID, &Settings{
|
||||
PeerLoginExpiration: time.Hour * 24 * 181,
|
||||
PeerLoginExpirationEnabled: false})
|
||||
PeerLoginExpirationEnabled: false,
|
||||
})
|
||||
require.Error(t, err, "expecting to fail when providing PeerLoginExpiration more than 180 days")
|
||||
}
|
||||
|
||||
@@ -1511,6 +1501,7 @@ func TestAccount_GetExpiredPeers(t *testing.T) {
|
||||
LoginExpired: false,
|
||||
},
|
||||
LastLogin: time.Now().Add(-30 * time.Minute),
|
||||
UserID: userID,
|
||||
},
|
||||
"peer-2": {
|
||||
ID: "peer-2",
|
||||
@@ -1521,6 +1512,7 @@ func TestAccount_GetExpiredPeers(t *testing.T) {
|
||||
LoginExpired: false,
|
||||
},
|
||||
LastLogin: time.Now().Add(-2 * time.Hour),
|
||||
UserID: userID,
|
||||
},
|
||||
|
||||
"peer-3": {
|
||||
@@ -1532,6 +1524,7 @@ func TestAccount_GetExpiredPeers(t *testing.T) {
|
||||
LoginExpired: false,
|
||||
},
|
||||
LastLogin: time.Now().Add(-1 * time.Hour),
|
||||
UserID: userID,
|
||||
},
|
||||
},
|
||||
expectedPeers: map[string]struct{}{
|
||||
@@ -1560,7 +1553,6 @@ func TestAccount_GetExpiredPeers(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAccount_GetPeersWithExpiration(t *testing.T) {
|
||||
@@ -1626,11 +1618,9 @@ func TestAccount_GetPeersWithExpiration(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAccount_GetNextPeerExpiration(t *testing.T) {
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
peers map[string]*Peer
|
||||
@@ -1754,10 +1744,8 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) {
|
||||
} else {
|
||||
assert.Equal(t, expiration, testCase.expectedNextExpiration)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func createManager(t *testing.T) (*DefaultAccountManager, error) {
|
||||
|
||||
@@ -19,6 +19,12 @@ const (
|
||||
RuleUpdated
|
||||
// RuleRemoved indicates that a user removed a rule
|
||||
RuleRemoved
|
||||
// PolicyAdded indicates that a user added a new policy
|
||||
PolicyAdded
|
||||
// PolicyUpdated indicates that a user updated a policy
|
||||
PolicyUpdated
|
||||
// PolicyRemoved indicates that a user removed a policy
|
||||
PolicyRemoved
|
||||
// SetupKeyCreated indicates that a user created a new setup key
|
||||
SetupKeyCreated
|
||||
// SetupKeyUpdated indicates that a user updated a setup key
|
||||
@@ -84,11 +90,11 @@ const (
|
||||
PeerAddedByUserMessage string = "Peer added"
|
||||
// PeerAddedWithSetupKeyMessage is a human-readable text message of the PeerAddedWithSetupKey activity
|
||||
PeerAddedWithSetupKeyMessage = PeerAddedByUserMessage
|
||||
//UserJoinedMessage is a human-readable text message of the UserJoined activity
|
||||
// UserJoinedMessage is a human-readable text message of the UserJoined activity
|
||||
UserJoinedMessage string = "User joined"
|
||||
//UserInvitedMessage is a human-readable text message of the UserInvited activity
|
||||
// UserInvitedMessage is a human-readable text message of the UserInvited activity
|
||||
UserInvitedMessage string = "User invited"
|
||||
//AccountCreatedMessage is a human-readable text message of the AccountCreated activity
|
||||
// AccountCreatedMessage is a human-readable text message of the AccountCreated activity
|
||||
AccountCreatedMessage string = "Account created"
|
||||
// PeerRemovedByUserMessage is a human-readable text message of the PeerRemovedByUser activity
|
||||
PeerRemovedByUserMessage string = "Peer deleted"
|
||||
@@ -98,6 +104,12 @@ const (
|
||||
RuleRemovedMessage string = "Rule deleted"
|
||||
// RuleUpdatedMessage is a human-readable text message of the RuleRemoved activity
|
||||
RuleUpdatedMessage string = "Rule updated"
|
||||
// PolicyAddedMessage is a human-readable text message of the PolicyAdded activity
|
||||
PolicyAddedMessage string = "Policy added"
|
||||
// PolicyRemovedMessage is a human-readable text message of the PolicyRemoved activity
|
||||
PolicyRemovedMessage string = "Policy deleted"
|
||||
// PolicyUpdatedMessage is a human-readable text message of the PolicyRemoved activity
|
||||
PolicyUpdatedMessage string = "Policy updated"
|
||||
// SetupKeyCreatedMessage is a human-readable text message of the SetupKeyCreated activity
|
||||
SetupKeyCreatedMessage string = "Setup key created"
|
||||
// SetupKeyUpdatedMessage is a human-readable text message of the SetupKeyUpdated activity
|
||||
@@ -182,6 +194,12 @@ func (a Activity) Message() string {
|
||||
return RuleRemovedMessage
|
||||
case RuleUpdated:
|
||||
return RuleUpdatedMessage
|
||||
case PolicyAdded:
|
||||
return PolicyAddedMessage
|
||||
case PolicyRemoved:
|
||||
return PolicyRemovedMessage
|
||||
case PolicyUpdated:
|
||||
return PolicyUpdatedMessage
|
||||
case SetupKeyCreated:
|
||||
return SetupKeyCreatedMessage
|
||||
case SetupKeyUpdated:
|
||||
@@ -266,6 +284,12 @@ func (a Activity) StringCode() string {
|
||||
return "rule.delete"
|
||||
case RuleUpdated:
|
||||
return "rule.update"
|
||||
case PolicyAdded:
|
||||
return "policy.add"
|
||||
case PolicyRemoved:
|
||||
return "policy.delete"
|
||||
case PolicyUpdated:
|
||||
return "policy.update"
|
||||
case SetupKeyCreated:
|
||||
return "setupkey.add"
|
||||
case SetupKeyRevoked:
|
||||
|
||||
@@ -244,11 +244,11 @@ func initTestDNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = am.AddPeer("", dnsAdminUserID, peer1)
|
||||
_, _, err = am.AddPeer("", dnsAdminUserID, peer1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = am.AddPeer("", dnsAdminUserID, peer2)
|
||||
_, _, err = am.AddPeer("", dnsAdminUserID, peer2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
"github.com/rs/xid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
"github.com/rs/xid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/util"
|
||||
)
|
||||
|
||||
@@ -81,7 +82,6 @@ func restore(file string) (*FileStore, error) {
|
||||
store.PeerID2AccountID = make(map[string]string)
|
||||
|
||||
for accountID, account := range store.Accounts {
|
||||
|
||||
if account.Settings == nil {
|
||||
account.Settings = &Settings{
|
||||
PeerLoginExpirationEnabled: false,
|
||||
@@ -113,6 +113,19 @@ func restore(file string) (*FileStore, error) {
|
||||
store.PrivateDomain2AccountID[account.Domain] = accountID
|
||||
}
|
||||
|
||||
// if no policies are defined, that means we need to migrate Rules to policies
|
||||
if len(account.Policies) == 0 {
|
||||
account.Policies = make([]*Policy, 0)
|
||||
for _, rule := range account.Rules {
|
||||
policy, err := RuleToPolicy(rule)
|
||||
if err != nil {
|
||||
log.Errorf("unable to migrate rule to policy: %v", err)
|
||||
continue
|
||||
}
|
||||
account.Policies = append(account.Policies, policy)
|
||||
}
|
||||
}
|
||||
|
||||
// for data migration. Can be removed once most base will be with labels
|
||||
existingLabels := account.getPeerDNSLabels()
|
||||
if len(existingLabels) != len(account.Peers) {
|
||||
@@ -140,6 +153,10 @@ func restore(file string) (*FileStore, error) {
|
||||
// Swap Peer.Key with Peer.ID in the Account.Peers map.
|
||||
migrationPeers := make(map[string]*Peer) // key to Peer
|
||||
for key, peer := range account.Peers {
|
||||
// set LastLogin for the peers that were onboarded before the peer login expiration feature
|
||||
if peer.LastLogin.IsZero() {
|
||||
peer.LastLogin = time.Now()
|
||||
}
|
||||
if peer.ID != "" {
|
||||
continue
|
||||
}
|
||||
@@ -149,7 +166,6 @@ func restore(file string) (*FileStore, error) {
|
||||
}
|
||||
|
||||
if len(migrationPeers) > 0 {
|
||||
|
||||
// swap Peer.Key with Peer.ID in the Account.Peers map.
|
||||
for key, peer := range migrationPeers {
|
||||
delete(account.Peers, key)
|
||||
@@ -165,6 +181,7 @@ func restore(file string) (*FileStore, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// detect routes that have Peer.Key as a reference and replace it with ID.
|
||||
for _, route := range account.Routes {
|
||||
if peer, ok := migrationPeers[route.Peer]; ok {
|
||||
@@ -247,6 +264,15 @@ func (s *FileStore) SaveAccount(account *Account) error {
|
||||
s.PrivateDomain2AccountID[accountCopy.Domain] = accountCopy.Id
|
||||
}
|
||||
|
||||
if accountCopy.Rules == nil {
|
||||
accountCopy.Rules = make(map[string]*Rule)
|
||||
}
|
||||
for _, policy := range accountCopy.Policies {
|
||||
for _, rule := range policy.Rules {
|
||||
accountCopy.Rules[rule.ID] = rule.ToRule()
|
||||
}
|
||||
}
|
||||
|
||||
return s.persist(s.storeFile)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/netbirdio/netbird/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type accounts struct {
|
||||
@@ -70,7 +71,6 @@ func TestNewStore(t *testing.T) {
|
||||
if store.UserID2AccountID == nil || len(store.UserID2AccountID) != 0 {
|
||||
t.Errorf("expected to create a new empty UserID2AccountID map when creating a new FileStore")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSaveAccount(t *testing.T) {
|
||||
@@ -109,7 +109,6 @@ func TestSaveAccount(t *testing.T) {
|
||||
if store.SetupKeyID2AccountID[setupKey.Key] == "" {
|
||||
t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount()")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestStore(t *testing.T) {
|
||||
@@ -124,6 +123,38 @@ func TestStore(t *testing.T) {
|
||||
Name: "peer name",
|
||||
Status: &PeerStatus{Connected: true, LastSeen: time.Now()},
|
||||
}
|
||||
account.Groups["all"] = &Group{
|
||||
ID: "all",
|
||||
Name: "all",
|
||||
Peers: []string{"testpeer"},
|
||||
}
|
||||
account.Rules["all"] = &Rule{
|
||||
ID: "all",
|
||||
Name: "all",
|
||||
Source: []string{"all"},
|
||||
Destination: []string{"all"},
|
||||
Flow: TrafficFlowBidirect,
|
||||
}
|
||||
account.Policies = append(account.Policies, &Policy{
|
||||
ID: "all",
|
||||
Name: "all",
|
||||
Enabled: true,
|
||||
Rules: []*PolicyRule{account.Rules["all"].ToPolicyRule()},
|
||||
})
|
||||
account.Policies = append(account.Policies, &Policy{
|
||||
ID: "dmz",
|
||||
Name: "dmz",
|
||||
Enabled: true,
|
||||
Rules: []*PolicyRule{
|
||||
{
|
||||
ID: "dmz",
|
||||
Name: "dmz",
|
||||
Enabled: true,
|
||||
Sources: []string{"all"},
|
||||
Destinations: []string{"all"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// SaveAccount should trigger persist
|
||||
err := store.SaveAccount(account)
|
||||
@@ -139,24 +170,48 @@ func TestStore(t *testing.T) {
|
||||
restoredAccount := restored.Accounts[account.Id]
|
||||
if restoredAccount == nil {
|
||||
t.Errorf("failed to restore a FileStore file - missing Account %s", account.Id)
|
||||
return
|
||||
}
|
||||
|
||||
if restoredAccount != nil && restoredAccount.Peers["testpeer"] == nil {
|
||||
if restoredAccount.Peers["testpeer"] == nil {
|
||||
t.Errorf("failed to restore a FileStore file - missing Peer testpeer")
|
||||
}
|
||||
|
||||
if restoredAccount != nil && restoredAccount.CreatedBy != "testuser" {
|
||||
if restoredAccount.CreatedBy != "testuser" {
|
||||
t.Errorf("failed to restore a FileStore file - missing Account CreatedBy")
|
||||
}
|
||||
|
||||
if restoredAccount != nil && restoredAccount.Users["testuser"] == nil {
|
||||
if restoredAccount.Users["testuser"] == nil {
|
||||
t.Errorf("failed to restore a FileStore file - missing User testuser")
|
||||
}
|
||||
|
||||
if restoredAccount != nil && restoredAccount.Network == nil {
|
||||
if restoredAccount.Network == nil {
|
||||
t.Errorf("failed to restore a FileStore file - missing Network")
|
||||
}
|
||||
|
||||
if restoredAccount.Groups["all"] == nil {
|
||||
t.Errorf("failed to restore a FileStore file - missing Group all")
|
||||
}
|
||||
|
||||
if restoredAccount.Rules["all"] == nil {
|
||||
t.Errorf("failed to restore a FileStore file - missing Rule all")
|
||||
return
|
||||
}
|
||||
|
||||
if restoredAccount.Rules["dmz"] == nil {
|
||||
t.Errorf("failed to restore a FileStore file - missing Rule dmz")
|
||||
return
|
||||
}
|
||||
assert.Equal(t, account.Rules["all"], restoredAccount.Rules["all"], "failed to restore a FileStore file - missing Rule all")
|
||||
assert.Equal(t, account.Rules["dmz"], restoredAccount.Rules["dmz"], "failed to restore a FileStore file - missing Rule dmz")
|
||||
|
||||
if len(restoredAccount.Policies) != 2 {
|
||||
t.Errorf("failed to restore a FileStore file - missing Policies")
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, account.Policies[0], restoredAccount.Policies[0], "failed to restore a FileStore file - missing Policy all")
|
||||
assert.Equal(t, account.Policies[1], restoredAccount.Policies[1], "failed to restore a FileStore file - missing Policy dmz")
|
||||
}
|
||||
|
||||
func TestRestore(t *testing.T) {
|
||||
@@ -191,6 +246,43 @@ func TestRestore(t *testing.T) {
|
||||
require.Len(t, store.PrivateDomain2AccountID, 1, "failed to restore a FileStore wrong PrivateDomain2AccountID mapping length")
|
||||
}
|
||||
|
||||
func TestRestorePolicies_Migration(t *testing.T) {
|
||||
storeDir := t.TempDir()
|
||||
|
||||
err := util.CopyFileContents("testdata/store_policy_migrate.json", filepath.Join(storeDir, "store.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
store, err := NewFileStore(storeDir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
account := store.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"]
|
||||
require.Len(t, account.Groups, 1, "failed to restore a FileStore file - missing Account Groups")
|
||||
require.Len(t, account.Rules, 1, "failed to restore a FileStore file - missing Account Rules")
|
||||
require.Len(t, account.Policies, 1, "failed to restore a FileStore file - missing Account Policies")
|
||||
|
||||
policy := account.Policies[0]
|
||||
require.Equal(t, policy.Name, "Default", "failed to restore a FileStore file - missing Account Policies Name")
|
||||
require.Equal(t, policy.Description,
|
||||
"This is a default rule that allows connections between all the resources",
|
||||
"failed to restore a FileStore file - missing Account Policies Description")
|
||||
expectedPolicy := policy.Copy()
|
||||
err = expectedPolicy.UpdateQueryFromRules()
|
||||
require.NoError(t, err, "failed to upldate query")
|
||||
require.Equal(t, policy.Query, expectedPolicy.Query, "failed to restore a FileStore file - missing Account Policies Query")
|
||||
require.Len(t, policy.Rules, 1, "failed to restore a FileStore file - missing Account Policy Rules")
|
||||
require.Equal(t, policy.Rules[0].Action, PolicyTrafficActionAccept, "failed to restore a FileStore file - missing Account Policies Action")
|
||||
require.Equal(t, policy.Rules[0].Destinations,
|
||||
[]string{"cfefqs706sqkneg59g3g"},
|
||||
"failed to restore a FileStore file - missing Account Policies Destinations")
|
||||
require.Equal(t, policy.Rules[0].Sources,
|
||||
[]string{"cfefqs706sqkneg59g3g"},
|
||||
"failed to restore a FileStore file - missing Account Policies Sources")
|
||||
}
|
||||
|
||||
func TestGetAccountByPrivateDomain(t *testing.T) {
|
||||
storeDir := t.TempDir()
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ func (g *Group) Copy() *Group {
|
||||
|
||||
// GetGroup object of the peers
|
||||
func (am *DefaultAccountManager) GetGroup(accountID, groupID string) (*Group, error) {
|
||||
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
@@ -72,7 +71,6 @@ func (am *DefaultAccountManager) GetGroup(accountID, groupID string) (*Group, er
|
||||
|
||||
// SaveGroup object of the peers
|
||||
func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *Group) error {
|
||||
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
@@ -112,8 +110,10 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *G
|
||||
continue
|
||||
}
|
||||
am.storeEvent(userID, peer.ID, accountID, activity.GroupAddedToPeer,
|
||||
map[string]any{"group": newGroup.Name, "group_id": newGroup.ID, "peer_ip": peer.IP.String(),
|
||||
"peer_fqdn": peer.FQDN(am.GetDNSDomain())})
|
||||
map[string]any{
|
||||
"group": newGroup.Name, "group_id": newGroup.ID, "peer_ip": peer.IP.String(),
|
||||
"peer_fqdn": peer.FQDN(am.GetDNSDomain()),
|
||||
})
|
||||
}
|
||||
|
||||
for _, p := range removedPeers {
|
||||
@@ -123,8 +123,10 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *G
|
||||
continue
|
||||
}
|
||||
am.storeEvent(userID, peer.ID, accountID, activity.GroupRemovedFromPeer,
|
||||
map[string]any{"group": newGroup.Name, "group_id": newGroup.ID, "peer_ip": peer.IP.String(),
|
||||
"peer_fqdn": peer.FQDN(am.GetDNSDomain())})
|
||||
map[string]any{
|
||||
"group": newGroup.Name, "group_id": newGroup.ID, "peer_ip": peer.IP.String(),
|
||||
"peer_fqdn": peer.FQDN(am.GetDNSDomain()),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -147,8 +149,8 @@ func difference(a, b []string) []string {
|
||||
|
||||
// UpdateGroup updates a group using a list of operations
|
||||
func (am *DefaultAccountManager) UpdateGroup(accountID string,
|
||||
groupID string, operations []GroupUpdateOperation) (*Group, error) {
|
||||
|
||||
groupID string, operations []GroupUpdateOperation,
|
||||
) (*Group, error) {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
@@ -198,7 +200,6 @@ func (am *DefaultAccountManager) UpdateGroup(accountID string,
|
||||
|
||||
// DeleteGroup object of the peers
|
||||
func (am *DefaultAccountManager) DeleteGroup(accountID, groupID string) error {
|
||||
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
@@ -219,7 +220,6 @@ func (am *DefaultAccountManager) DeleteGroup(accountID, groupID string) error {
|
||||
|
||||
// ListGroups objects of the peers
|
||||
func (am *DefaultAccountManager) ListGroups(accountID string) ([]*Group, error) {
|
||||
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
@@ -238,7 +238,6 @@ func (am *DefaultAccountManager) ListGroups(accountID string) ([]*Group, error)
|
||||
|
||||
// GroupAddPeer appends peer to the group
|
||||
func (am *DefaultAccountManager) GroupAddPeer(accountID, groupID, peerID string) error {
|
||||
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
@@ -273,7 +272,6 @@ func (am *DefaultAccountManager) GroupAddPeer(accountID, groupID, peerID string)
|
||||
|
||||
// GroupDeletePeer removes peer from the group
|
||||
func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerKey string) error {
|
||||
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
@@ -302,7 +300,6 @@ func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerKey str
|
||||
|
||||
// GroupListPeers returns list of the peers from the group
|
||||
func (am *DefaultAccountManager) GroupListPeers(accountID, groupID string) ([]*Peer, error) {
|
||||
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
pb "github.com/golang/protobuf/proto" //nolint
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -118,44 +119,18 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi
|
||||
log.Debugf("Sync request from peer [%s] [%s]", req.WgPubKey, p.Addr.String())
|
||||
}
|
||||
|
||||
peerKey, err := wgtypes.ParseKey(req.GetWgPubKey())
|
||||
if err != nil {
|
||||
log.Warnf("error while parsing peer's Wireguard public key %s on Sync request.", peerKey.String())
|
||||
return status.Errorf(codes.InvalidArgument, "provided wgPubKey %s is invalid", peerKey.String())
|
||||
}
|
||||
|
||||
peer, err := s.accountManager.GetPeerByKey(peerKey.String())
|
||||
if err != nil {
|
||||
p, _ := gRPCPeer.FromContext(srv.Context())
|
||||
msg := status.Errorf(codes.PermissionDenied, "provided peer with the key wgPubKey %s is not registered, remote addr is %s", peerKey.String(), p.Addr.String())
|
||||
log.Debug(msg)
|
||||
return msg
|
||||
}
|
||||
|
||||
account, err := s.accountManager.GetAccountByPeerID(peer.ID)
|
||||
if err != nil {
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
expired, left := peer.LoginExpired(account.Settings.PeerLoginExpiration)
|
||||
expired = account.Settings.PeerLoginExpirationEnabled && expired
|
||||
if peer.UserID != "" && (expired || peer.Status.LoginExpired) {
|
||||
err = s.accountManager.MarkPeerLoginExpired(peerKey.String(), true)
|
||||
if err != nil {
|
||||
log.Warnf("failed marking peer login expired %s %v", peerKey, err)
|
||||
}
|
||||
return status.Errorf(codes.PermissionDenied, "peer login has expired %v ago. Please log in once more", left)
|
||||
}
|
||||
|
||||
syncReq := &proto.SyncRequest{}
|
||||
err = encryption.DecryptMessage(peerKey, s.wgKey, req.Body, syncReq)
|
||||
peerKey, err := s.parseRequest(req, syncReq)
|
||||
if err != nil {
|
||||
p, _ := gRPCPeer.FromContext(srv.Context())
|
||||
msg := status.Errorf(codes.InvalidArgument, "invalid request message from %s,remote addr is %s", peerKey.String(), p.Addr.String())
|
||||
log.Debug(msg)
|
||||
return msg
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.sendInitialSync(peerKey, peer, srv)
|
||||
peer, netMap, err := s.accountManager.SyncPeer(PeerSync{WireGuardPubKey: peerKey.String()})
|
||||
if err != nil {
|
||||
return mapError(err)
|
||||
}
|
||||
|
||||
err = s.sendInitialSync(peerKey, peer, netMap, srv)
|
||||
if err != nil {
|
||||
log.Debugf("error while sending initial sync for %s: %v", peerKey.String(), err)
|
||||
return err
|
||||
@@ -218,7 +193,7 @@ func (s *GRPCServer) validateToken(jwtToken string) (string, error) {
|
||||
|
||||
token, err := s.jwtMiddleware.ValidateAndParse(jwtToken)
|
||||
if err != nil {
|
||||
return "", status.Errorf(codes.Internal, "invalid jwt token, err: %v", err)
|
||||
return "", status.Errorf(codes.InvalidArgument, "invalid jwt token, err: %v", err)
|
||||
}
|
||||
claims := s.jwtClaimsExtractor.FromToken(token)
|
||||
// we need to call this method because if user is new, we will automatically add it to existing or create a new account
|
||||
@@ -230,84 +205,52 @@ func (s *GRPCServer) validateToken(jwtToken string) (string, error) {
|
||||
return claims.UserId, nil
|
||||
}
|
||||
|
||||
func (s *GRPCServer) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest) (*Peer, error) {
|
||||
var (
|
||||
reqSetupKey string
|
||||
userID string
|
||||
err error
|
||||
)
|
||||
|
||||
if req.GetJwtToken() != "" {
|
||||
log.Debugln("using jwt token to register peer")
|
||||
userID, err = s.validateToken(req.JwtToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// maps internal internalStatus.Error to gRPC status.Error
|
||||
func mapError(err error) error {
|
||||
if e, ok := internalStatus.FromError(err); ok {
|
||||
switch e.Type() {
|
||||
case internalStatus.PermissionDenied:
|
||||
return status.Errorf(codes.PermissionDenied, e.Message)
|
||||
case internalStatus.Unauthorized:
|
||||
return status.Errorf(codes.PermissionDenied, e.Message)
|
||||
case internalStatus.Unauthenticated:
|
||||
return status.Errorf(codes.PermissionDenied, e.Message)
|
||||
case internalStatus.PreconditionFailed:
|
||||
return status.Errorf(codes.FailedPrecondition, e.Message)
|
||||
case internalStatus.NotFound:
|
||||
return status.Errorf(codes.NotFound, e.Message)
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
log.Debugln("using setup key to register peer")
|
||||
reqSetupKey = req.GetSetupKey()
|
||||
userID = ""
|
||||
}
|
||||
return status.Errorf(codes.Internal, "failed handling request")
|
||||
}
|
||||
|
||||
meta := req.GetMeta()
|
||||
if meta == nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "peer meta data was not provided")
|
||||
func extractPeerMeta(loginReq *proto.LoginRequest) PeerSystemMeta {
|
||||
return PeerSystemMeta{
|
||||
Hostname: loginReq.GetMeta().GetHostname(),
|
||||
GoOS: loginReq.GetMeta().GetGoOS(),
|
||||
Kernel: loginReq.GetMeta().GetKernel(),
|
||||
Core: loginReq.GetMeta().GetCore(),
|
||||
Platform: loginReq.GetMeta().GetPlatform(),
|
||||
OS: loginReq.GetMeta().GetOS(),
|
||||
WtVersion: loginReq.GetMeta().GetWiretrusteeVersion(),
|
||||
UIVersion: loginReq.GetMeta().GetUiVersion(),
|
||||
}
|
||||
}
|
||||
|
||||
var sshKey []byte
|
||||
if req.GetPeerKeys() != nil {
|
||||
sshKey = req.GetPeerKeys().GetSshPubKey()
|
||||
}
|
||||
|
||||
peer, err := s.accountManager.AddPeer(reqSetupKey, userID, &Peer{
|
||||
Key: peerKey.String(),
|
||||
Name: meta.GetHostname(),
|
||||
SSHKey: string(sshKey),
|
||||
Meta: PeerSystemMeta{
|
||||
Hostname: meta.GetHostname(),
|
||||
GoOS: meta.GetGoOS(),
|
||||
Kernel: meta.GetKernel(),
|
||||
Core: meta.GetCore(),
|
||||
Platform: meta.GetPlatform(),
|
||||
OS: meta.GetOS(),
|
||||
WtVersion: meta.GetWiretrusteeVersion(),
|
||||
UIVersion: meta.GetUiVersion(),
|
||||
},
|
||||
})
|
||||
func (s *GRPCServer) parseRequest(req *proto.EncryptedMessage, parsed pb.Message) (wgtypes.Key, error) {
|
||||
peerKey, err := wgtypes.ParseKey(req.GetWgPubKey())
|
||||
if err != nil {
|
||||
if e, ok := internalStatus.FromError(err); ok {
|
||||
switch e.Type() {
|
||||
case internalStatus.PreconditionFailed:
|
||||
return nil, status.Errorf(codes.FailedPrecondition, e.Message)
|
||||
case internalStatus.NotFound:
|
||||
return nil, status.Errorf(codes.NotFound, e.Message)
|
||||
default:
|
||||
}
|
||||
}
|
||||
return nil, status.Errorf(codes.Internal, "failed registering new peer")
|
||||
log.Warnf("error while parsing peer's WireGuard public key %s.", req.WgPubKey)
|
||||
return wgtypes.Key{}, status.Errorf(codes.InvalidArgument, "provided wgPubKey %s is invalid", req.WgPubKey)
|
||||
}
|
||||
|
||||
// todo move to DefaultAccountManager the code below
|
||||
networkMap, err := s.accountManager.GetNetworkMap(peer.ID)
|
||||
err = encryption.DecryptMessage(peerKey, s.wgKey, req.Body, parsed)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "unable to fetch network map after registering peer, error: %v", err)
|
||||
}
|
||||
// notify other peers of our registration
|
||||
for _, remotePeer := range networkMap.Peers {
|
||||
remotePeerNetworkMap, err := s.accountManager.GetNetworkMap(remotePeer.ID)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "unable to fetch network map after registering peer, error: %v", err)
|
||||
}
|
||||
|
||||
update := toSyncResponse(s.config, remotePeer, nil, remotePeerNetworkMap, s.accountManager.GetDNSDomain())
|
||||
err = s.peersUpdateManager.SendUpdate(remotePeer.ID, &UpdateMessage{Update: update})
|
||||
if err != nil {
|
||||
// todo rethink if we should keep this return
|
||||
return nil, status.Errorf(codes.Internal, "unable to send update after registering peer, error: %v", err)
|
||||
}
|
||||
return wgtypes.Key{}, status.Errorf(codes.InvalidArgument, "invalid request message")
|
||||
}
|
||||
|
||||
return peer, nil
|
||||
return peerKey, nil
|
||||
}
|
||||
|
||||
// Login endpoint first checks whether peer is registered under any account
|
||||
@@ -323,113 +266,55 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p
|
||||
log.Debugf("Login request from peer [%s] [%s]", req.WgPubKey, p.Addr.String())
|
||||
}
|
||||
|
||||
peerKey, err := wgtypes.ParseKey(req.GetWgPubKey())
|
||||
if err != nil {
|
||||
log.Warnf("error while parsing peer's Wireguard public key %s on Sync request.", req.WgPubKey)
|
||||
return nil, status.Errorf(codes.InvalidArgument, "provided wgPubKey %s is invalid", req.WgPubKey)
|
||||
}
|
||||
|
||||
loginReq := &proto.LoginRequest{}
|
||||
err = encryption.DecryptMessage(peerKey, s.wgKey, req.Body, loginReq)
|
||||
peerKey, err := s.parseRequest(req, loginReq)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid request message")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peer, err := s.accountManager.GetPeerByKey(peerKey.String())
|
||||
if err != nil {
|
||||
if errStatus, ok := internalStatus.FromError(err); ok && errStatus.Type() == internalStatus.NotFound {
|
||||
// peer doesn't exist -> check if setup key was provided
|
||||
if loginReq.GetJwtToken() == "" && loginReq.GetSetupKey() == "" {
|
||||
// absent setup key or jwt -> permission denied
|
||||
p, _ := gRPCPeer.FromContext(ctx)
|
||||
msg := status.Errorf(codes.PermissionDenied,
|
||||
"provided peer with the key wgPubKey %s is not registered and no setup key or jwt was provided,"+
|
||||
" remote addr is %s", peerKey.String(), p.Addr.String())
|
||||
log.Debug(msg)
|
||||
return nil, msg
|
||||
}
|
||||
if loginReq.GetMeta() == nil {
|
||||
msg := status.Errorf(codes.FailedPrecondition,
|
||||
"peer system meta has to be provided to log in. Peer %s, remote addr %s", peerKey.String(),
|
||||
p.Addr.String())
|
||||
log.Warn(msg)
|
||||
return nil, msg
|
||||
}
|
||||
|
||||
// setup key or jwt is present -> try normal registration flow
|
||||
peer, err = s.registerPeer(peerKey, loginReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else {
|
||||
return nil, status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
} else if loginReq.GetMeta() != nil {
|
||||
// update peer's system meta data on Login
|
||||
err = s.accountManager.UpdatePeerMeta(peer.ID, PeerSystemMeta{
|
||||
Hostname: loginReq.GetMeta().GetHostname(),
|
||||
GoOS: loginReq.GetMeta().GetGoOS(),
|
||||
Kernel: loginReq.GetMeta().GetKernel(),
|
||||
Core: loginReq.GetMeta().GetCore(),
|
||||
Platform: loginReq.GetMeta().GetPlatform(),
|
||||
OS: loginReq.GetMeta().GetOS(),
|
||||
WtVersion: loginReq.GetMeta().GetWiretrusteeVersion(),
|
||||
UIVersion: loginReq.GetMeta().GetUiVersion(),
|
||||
},
|
||||
)
|
||||
userID := ""
|
||||
// JWT token is not always provided, it is fine for userID to be empty cuz it might be that peer is already registered,
|
||||
// or it uses a setup key to register.
|
||||
if loginReq.GetJwtToken() != "" {
|
||||
userID, err = s.validateToken(loginReq.GetJwtToken())
|
||||
if err != nil {
|
||||
log.Errorf("failed updating peer system meta data %s", peerKey.String())
|
||||
return nil, status.Error(codes.Internal, "internal server error")
|
||||
log.Warnf("failed validating JWT token sent from peer %s", peerKey)
|
||||
return nil, mapError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// check if peer login has expired
|
||||
account, err := s.accountManager.GetAccountByPeerID(peer.ID)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
|
||||
expired, left := peer.LoginExpired(account.Settings.PeerLoginExpiration)
|
||||
expired = account.Settings.PeerLoginExpirationEnabled && expired
|
||||
if peer.UserID != "" && (expired || peer.Status.LoginExpired) {
|
||||
// it might be that peer expired but user has logged in already, check token then
|
||||
if loginReq.GetJwtToken() == "" {
|
||||
err = s.accountManager.MarkPeerLoginExpired(peerKey.String(), true)
|
||||
if err != nil {
|
||||
log.Warnf("failed marking peer login expired %s %v", peerKey, err)
|
||||
}
|
||||
return nil, status.Errorf(codes.PermissionDenied,
|
||||
"peer login has expired %v ago. Please log in once more", left)
|
||||
}
|
||||
_, err = s.validateToken(loginReq.GetJwtToken())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.accountManager.UpdatePeerLastLogin(peer.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var sshKey []byte
|
||||
if loginReq.GetPeerKeys() != nil {
|
||||
sshKey = loginReq.GetPeerKeys().GetSshPubKey()
|
||||
}
|
||||
|
||||
if len(sshKey) > 0 {
|
||||
err = s.accountManager.UpdatePeerSSHKey(peer.ID, string(sshKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
network, err := s.accountManager.GetPeerNetwork(peer.ID)
|
||||
peer, netMap, err := s.accountManager.LoginPeer(PeerLogin{
|
||||
WireGuardPubKey: peerKey.String(),
|
||||
SSHKey: string(sshKey),
|
||||
Meta: extractPeerMeta(loginReq),
|
||||
UserID: userID,
|
||||
SetupKey: loginReq.GetSetupKey(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed getting peer network on login")
|
||||
log.Warnf("failed logging in peer %s", peerKey)
|
||||
return nil, mapError(err)
|
||||
}
|
||||
|
||||
// if peer has reached this point then it has logged in
|
||||
loginResp := &proto.LoginResponse{
|
||||
WiretrusteeConfig: toWiretrusteeConfig(s.config, nil),
|
||||
PeerConfig: toPeerConfig(peer, network, s.accountManager.GetDNSDomain()),
|
||||
PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain()),
|
||||
}
|
||||
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, loginResp)
|
||||
if err != nil {
|
||||
log.Warnf("failed encrypting peer %s message", peer.ID)
|
||||
return nil, status.Errorf(codes.Internal, "failed logging in peer")
|
||||
}
|
||||
|
||||
@@ -533,6 +418,8 @@ func toSyncResponse(config *Config, peer *Peer, turnCredentials *TURNCredentials
|
||||
|
||||
dnsUpdate := toProtocolDNSConfig(networkMap.DNSConfig)
|
||||
|
||||
offlinePeers := toRemotePeerConfig(networkMap.OfflinePeers, dnsName)
|
||||
|
||||
return &proto.SyncResponse{
|
||||
WiretrusteeConfig: wtConfig,
|
||||
PeerConfig: pConfig,
|
||||
@@ -542,6 +429,7 @@ func toSyncResponse(config *Config, peer *Peer, turnCredentials *TURNCredentials
|
||||
Serial: networkMap.Network.CurrentSerial(),
|
||||
PeerConfig: pConfig,
|
||||
RemotePeers: remotePeers,
|
||||
OfflinePeers: offlinePeers,
|
||||
RemotePeersIsEmpty: len(remotePeers) == 0,
|
||||
Routes: routesUpdate,
|
||||
DNSConfig: dnsUpdate,
|
||||
@@ -555,13 +443,7 @@ func (s *GRPCServer) IsHealthy(ctx context.Context, req *proto.Empty) (*proto.Em
|
||||
}
|
||||
|
||||
// sendInitialSync sends initial proto.SyncResponse to the peer requesting synchronization
|
||||
func (s *GRPCServer) sendInitialSync(peerKey wgtypes.Key, peer *Peer, srv proto.ManagementService_SyncServer) error {
|
||||
networkMap, err := s.accountManager.GetNetworkMap(peer.ID)
|
||||
if err != nil {
|
||||
log.Warnf("error getting a list of peers for a peer %s", peer.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *GRPCServer) sendInitialSync(peerKey wgtypes.Key, peer *Peer, networkMap *NetworkMap, srv proto.ManagementService_SyncServer) error {
|
||||
// make secret time based TURN credentials optional
|
||||
var turnCredentials *TURNCredentials
|
||||
if s.config.TURNConfig.TimeBasedCredentials {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: NetBird REST API
|
||||
description: API to manipulate groups, rules and retrieve information about peers and users
|
||||
description: API to manipulate groups, rules, policies and retrieve information about peers and users
|
||||
version: 0.0.1
|
||||
tags:
|
||||
- name: Users
|
||||
@@ -14,6 +14,8 @@ tags:
|
||||
description: Interact with and view information about groups.
|
||||
- name: Rules
|
||||
description: Interact with and view information about rules.
|
||||
- name: Policies
|
||||
description: Interact with and view information about policies.
|
||||
- name: Routes
|
||||
description: Interact with and view information about routes.
|
||||
- name: DNS
|
||||
@@ -393,6 +395,77 @@ components:
|
||||
enum: [ "name","description","disabled","flow","sources","destinations" ]
|
||||
required:
|
||||
- path
|
||||
PolicyRule:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: Rule ID
|
||||
type: string
|
||||
name:
|
||||
description: Rule name identifier
|
||||
type: string
|
||||
description:
|
||||
description: Rule friendly description
|
||||
type: string
|
||||
enabled:
|
||||
description: Rules status
|
||||
type: boolean
|
||||
sources:
|
||||
description: policy source groups
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/GroupMinimum'
|
||||
destinations:
|
||||
description: policy destination groups
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/GroupMinimum'
|
||||
action:
|
||||
description: policy accept or drops packets
|
||||
type: string
|
||||
enum: ["accept","drop"]
|
||||
required:
|
||||
- name
|
||||
- sources
|
||||
- destinations
|
||||
- action
|
||||
- enabled
|
||||
PolicyMinimum:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
description: Policy name identifier
|
||||
type: string
|
||||
description:
|
||||
description: Policy friendly description
|
||||
type: string
|
||||
enabled:
|
||||
description: Policy status
|
||||
type: boolean
|
||||
query:
|
||||
description: Policy Rego query
|
||||
type: string
|
||||
rules:
|
||||
description: Policy rule object for policy UI editor
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/PolicyRule'
|
||||
required:
|
||||
- name
|
||||
- description
|
||||
- enabled
|
||||
- query
|
||||
- rules
|
||||
Policy:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PolicyMinimum'
|
||||
- type: object
|
||||
properties:
|
||||
id:
|
||||
description: Policy ID
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
RouteRequest:
|
||||
type: object
|
||||
properties:
|
||||
@@ -574,12 +647,12 @@ components:
|
||||
"setupkey.peer.add", "setupkey.add", "setupkey.update", "setupkey.revoke", "setupkey.overuse",
|
||||
"setupkey.group.delete", "setupkey.group.add",
|
||||
"rule.add", "rule.delete", "rule.update",
|
||||
"policy.add", "policy.delete", "policy.update",
|
||||
"group.add", "group.update", "dns.setting.disabled.management.group.add", "dns.setting.disabled.management.group.delete",
|
||||
"account.create", "account.setting.peer.login.expiration.update", "account.setting.peer.login.expiration.disable", "account.setting.peer.login.expiration.enable",
|
||||
"route.add", "route.delete", "route.update",
|
||||
"nameserver.group.add", "nameserver.group.delete", "nameserver.group.update",
|
||||
"peer.ssh.disable", "peer.ssh.enable", "peer.rename", "peer.login.expiration.disable", "peer.login.expiration.enable"
|
||||
]
|
||||
"peer.ssh.disable", "peer.ssh.enable", "peer.rename", "peer.login.expiration.disable", "peer.login.expiration.enable" ]
|
||||
initiator_id:
|
||||
description: The ID of the initiator of the event. E.g., an ID of a user that triggered the event.
|
||||
type: string
|
||||
@@ -1337,41 +1410,6 @@ paths:
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
patch:
|
||||
summary: Update information about a Rule
|
||||
tags: [ Rules ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The Rule ID
|
||||
requestBody:
|
||||
description: Update Rule request using a list of json patch objects
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RulePatchOperation'
|
||||
responses:
|
||||
'200':
|
||||
description: A Rule object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Rule'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
delete:
|
||||
summary: Delete a Rule
|
||||
tags: [ Rules ]
|
||||
@@ -1396,7 +1434,134 @@ paths:
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
|
||||
/api/policies:
|
||||
get:
|
||||
summary: Returns a list of all Policies
|
||||
tags: [ Policies ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
responses:
|
||||
'200':
|
||||
description: A JSON Array of Policies
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Policy'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
post:
|
||||
summary: Creates a Policy
|
||||
tags: [ Policies ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
requestBody:
|
||||
description: New Policy request
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PolicyMinimum'
|
||||
responses:
|
||||
'200':
|
||||
description: A Policy Object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Policy'
|
||||
/api/policies/{id}:
|
||||
get:
|
||||
summary: Get information about a Policies
|
||||
tags: [ Policies ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The Policy ID
|
||||
responses:
|
||||
'200':
|
||||
description: A Policy object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Policy'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
put:
|
||||
summary: Update/Replace a Policy
|
||||
tags: [ Policies ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The Policy ID
|
||||
requestBody:
|
||||
description: Update Policy request
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/PolicyMinimum'
|
||||
responses:
|
||||
'200':
|
||||
description: A Policy object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Policy'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
delete:
|
||||
summary: Delete a Policy
|
||||
tags: [ Policies ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The Policy ID
|
||||
responses:
|
||||
'200':
|
||||
description: Delete status code
|
||||
content: { }
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/routes:
|
||||
get:
|
||||
summary: Returns a list of all routes
|
||||
|
||||
@@ -29,6 +29,9 @@ const (
|
||||
EventActivityCodePeerRename EventActivityCode = "peer.rename"
|
||||
EventActivityCodePeerSshDisable EventActivityCode = "peer.ssh.disable"
|
||||
EventActivityCodePeerSshEnable EventActivityCode = "peer.ssh.enable"
|
||||
EventActivityCodePolicyAdd EventActivityCode = "policy.add"
|
||||
EventActivityCodePolicyDelete EventActivityCode = "policy.delete"
|
||||
EventActivityCodePolicyUpdate EventActivityCode = "policy.update"
|
||||
EventActivityCodeRouteAdd EventActivityCode = "route.add"
|
||||
EventActivityCodeRouteDelete EventActivityCode = "route.delete"
|
||||
EventActivityCodeRouteUpdate EventActivityCode = "route.update"
|
||||
@@ -94,6 +97,12 @@ const (
|
||||
PatchMinimumOpReplace PatchMinimumOp = "replace"
|
||||
)
|
||||
|
||||
// Defines values for PolicyRuleAction.
|
||||
const (
|
||||
PolicyRuleActionAccept PolicyRuleAction = "accept"
|
||||
PolicyRuleActionDrop PolicyRuleAction = "drop"
|
||||
)
|
||||
|
||||
// Defines values for RoutePatchOperationOp.
|
||||
const (
|
||||
RoutePatchOperationOpAdd RoutePatchOperationOp = "add"
|
||||
@@ -113,23 +122,6 @@ const (
|
||||
RoutePatchOperationPathPeer RoutePatchOperationPath = "peer"
|
||||
)
|
||||
|
||||
// Defines values for RulePatchOperationOp.
|
||||
const (
|
||||
RulePatchOperationOpAdd RulePatchOperationOp = "add"
|
||||
RulePatchOperationOpRemove RulePatchOperationOp = "remove"
|
||||
RulePatchOperationOpReplace RulePatchOperationOp = "replace"
|
||||
)
|
||||
|
||||
// Defines values for RulePatchOperationPath.
|
||||
const (
|
||||
RulePatchOperationPathDescription RulePatchOperationPath = "description"
|
||||
RulePatchOperationPathDestinations RulePatchOperationPath = "destinations"
|
||||
RulePatchOperationPathDisabled RulePatchOperationPath = "disabled"
|
||||
RulePatchOperationPathFlow RulePatchOperationPath = "flow"
|
||||
RulePatchOperationPathName RulePatchOperationPath = "name"
|
||||
RulePatchOperationPathSources RulePatchOperationPath = "sources"
|
||||
)
|
||||
|
||||
// Defines values for UserStatus.
|
||||
const (
|
||||
UserStatusActive UserStatus = "active"
|
||||
@@ -387,6 +379,72 @@ type PeerMinimum struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Policy defines model for Policy.
|
||||
type Policy struct {
|
||||
// Description Policy friendly description
|
||||
Description string `json:"description"`
|
||||
|
||||
// Enabled Policy status
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Id Policy ID
|
||||
Id string `json:"id"`
|
||||
|
||||
// Name Policy name identifier
|
||||
Name string `json:"name"`
|
||||
|
||||
// Query Policy Rego query
|
||||
Query string `json:"query"`
|
||||
|
||||
// Rules Policy rule object for policy UI editor
|
||||
Rules []PolicyRule `json:"rules"`
|
||||
}
|
||||
|
||||
// PolicyMinimum defines model for PolicyMinimum.
|
||||
type PolicyMinimum struct {
|
||||
// Description Policy friendly description
|
||||
Description string `json:"description"`
|
||||
|
||||
// Enabled Policy status
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Name Policy name identifier
|
||||
Name string `json:"name"`
|
||||
|
||||
// Query Policy Rego query
|
||||
Query string `json:"query"`
|
||||
|
||||
// Rules Policy rule object for policy UI editor
|
||||
Rules []PolicyRule `json:"rules"`
|
||||
}
|
||||
|
||||
// PolicyRule defines model for PolicyRule.
|
||||
type PolicyRule struct {
|
||||
// Action policy accept or drops packets
|
||||
Action PolicyRuleAction `json:"action"`
|
||||
|
||||
// Description Rule friendly description
|
||||
Description *string `json:"description,omitempty"`
|
||||
|
||||
// Destinations policy destination groups
|
||||
Destinations []GroupMinimum `json:"destinations"`
|
||||
|
||||
// Enabled Rules status
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Id Rule ID
|
||||
Id *string `json:"id,omitempty"`
|
||||
|
||||
// Name Rule name identifier
|
||||
Name string `json:"name"`
|
||||
|
||||
// Sources policy source groups
|
||||
Sources []GroupMinimum `json:"sources"`
|
||||
}
|
||||
|
||||
// PolicyRuleAction policy accept or drops packets
|
||||
type PolicyRuleAction string
|
||||
|
||||
// Route defines model for Route.
|
||||
type Route struct {
|
||||
// Description Route description
|
||||
@@ -504,24 +562,6 @@ type RuleMinimum struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// RulePatchOperation defines model for RulePatchOperation.
|
||||
type RulePatchOperation struct {
|
||||
// Op Patch operation type
|
||||
Op RulePatchOperationOp `json:"op"`
|
||||
|
||||
// Path Rule field to update in form /<field>
|
||||
Path RulePatchOperationPath `json:"path"`
|
||||
|
||||
// Value Values to be applied
|
||||
Value []string `json:"value"`
|
||||
}
|
||||
|
||||
// RulePatchOperationOp Patch operation type
|
||||
type RulePatchOperationOp string
|
||||
|
||||
// RulePatchOperationPath Rule field to update in form /<field>
|
||||
type RulePatchOperationPath string
|
||||
|
||||
// SetupKey defines model for SetupKey.
|
||||
type SetupKey struct {
|
||||
// AutoGroups Setup key groups to auto-assign to peers registered with this key
|
||||
@@ -666,6 +706,12 @@ type PutApiPeersIdJSONBody struct {
|
||||
SshEnabled bool `json:"ssh_enabled"`
|
||||
}
|
||||
|
||||
// PostApiPoliciesJSONBody defines parameters for PostApiPolicies.
|
||||
type PostApiPoliciesJSONBody = PolicyMinimum
|
||||
|
||||
// PutApiPoliciesIdJSONBody defines parameters for PutApiPoliciesId.
|
||||
type PutApiPoliciesIdJSONBody = PolicyMinimum
|
||||
|
||||
// PatchApiRoutesIdJSONBody defines parameters for PatchApiRoutesId.
|
||||
type PatchApiRoutesIdJSONBody = []RoutePatchOperation
|
||||
|
||||
@@ -686,9 +732,6 @@ type PostApiRulesJSONBody struct {
|
||||
Sources *[]string `json:"sources,omitempty"`
|
||||
}
|
||||
|
||||
// PatchApiRulesIdJSONBody defines parameters for PatchApiRulesId.
|
||||
type PatchApiRulesIdJSONBody = []RulePatchOperation
|
||||
|
||||
// PutApiRulesIdJSONBody defines parameters for PutApiRulesId.
|
||||
type PutApiRulesIdJSONBody struct {
|
||||
// Description Rule friendly description
|
||||
@@ -733,6 +776,12 @@ type PutApiGroupsIdJSONRequestBody PutApiGroupsIdJSONBody
|
||||
// PutApiPeersIdJSONRequestBody defines body for PutApiPeersId for application/json ContentType.
|
||||
type PutApiPeersIdJSONRequestBody PutApiPeersIdJSONBody
|
||||
|
||||
// PostApiPoliciesJSONRequestBody defines body for PostApiPolicies for application/json ContentType.
|
||||
type PostApiPoliciesJSONRequestBody = PostApiPoliciesJSONBody
|
||||
|
||||
// PutApiPoliciesIdJSONRequestBody defines body for PutApiPoliciesId for application/json ContentType.
|
||||
type PutApiPoliciesIdJSONRequestBody = PutApiPoliciesIdJSONBody
|
||||
|
||||
// PostApiRoutesJSONRequestBody defines body for PostApiRoutes for application/json ContentType.
|
||||
type PostApiRoutesJSONRequestBody = RouteRequest
|
||||
|
||||
@@ -745,9 +794,6 @@ type PutApiRoutesIdJSONRequestBody = RouteRequest
|
||||
// PostApiRulesJSONRequestBody defines body for PostApiRules for application/json ContentType.
|
||||
type PostApiRulesJSONRequestBody PostApiRulesJSONBody
|
||||
|
||||
// PatchApiRulesIdJSONRequestBody defines body for PatchApiRulesId for application/json ContentType.
|
||||
type PatchApiRulesIdJSONRequestBody = PatchApiRulesIdJSONBody
|
||||
|
||||
// PutApiRulesIdJSONRequestBody defines body for PutApiRulesId for application/json ContentType.
|
||||
type PutApiRulesIdJSONRequestBody PutApiRulesIdJSONBody
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ func APIHandler(accountManager s.AccountManager, appMetrics telemetry.AppMetrics
|
||||
api.addUsersEndpoint()
|
||||
api.addSetupKeysEndpoint()
|
||||
api.addRulesEndpoint()
|
||||
api.addPoliciesEndpoint()
|
||||
api.addGroupsEndpoint()
|
||||
api.addRoutesEndpoint()
|
||||
api.addDNSNameserversEndpoint()
|
||||
@@ -122,11 +123,19 @@ func (apiHandler *apiHandler) addRulesEndpoint() {
|
||||
apiHandler.Router.HandleFunc("/rules", rulesHandler.GetAllRules).Methods("GET", "OPTIONS")
|
||||
apiHandler.Router.HandleFunc("/rules", rulesHandler.CreateRule).Methods("POST", "OPTIONS")
|
||||
apiHandler.Router.HandleFunc("/rules/{id}", rulesHandler.UpdateRule).Methods("PUT", "OPTIONS")
|
||||
apiHandler.Router.HandleFunc("/rules/{id}", rulesHandler.PatchRule).Methods("PATCH", "OPTIONS")
|
||||
apiHandler.Router.HandleFunc("/rules/{id}", rulesHandler.GetRule).Methods("GET", "OPTIONS")
|
||||
apiHandler.Router.HandleFunc("/rules/{id}", rulesHandler.DeleteRule).Methods("DELETE", "OPTIONS")
|
||||
}
|
||||
|
||||
func (apiHandler *apiHandler) addPoliciesEndpoint() {
|
||||
policiesHandler := NewPoliciesHandler(apiHandler.AccountManager, apiHandler.AuthCfg)
|
||||
apiHandler.Router.HandleFunc("/policies", policiesHandler.GetAllPolicies).Methods("GET", "OPTIONS")
|
||||
apiHandler.Router.HandleFunc("/policies", policiesHandler.CreatePolicy).Methods("POST", "OPTIONS")
|
||||
apiHandler.Router.HandleFunc("/policies/{id}", policiesHandler.UpdatePolicy).Methods("PUT", "OPTIONS")
|
||||
apiHandler.Router.HandleFunc("/policies/{id}", policiesHandler.GetPolicy).Methods("GET", "OPTIONS")
|
||||
apiHandler.Router.HandleFunc("/policies/{id}", policiesHandler.DeletePolicy).Methods("DELETE", "OPTIONS")
|
||||
}
|
||||
|
||||
func (apiHandler *apiHandler) addGroupsEndpoint() {
|
||||
groupsHandler := NewGroupsHandler(apiHandler.AccountManager, apiHandler.AuthCfg)
|
||||
apiHandler.Router.HandleFunc("/groups", groupsHandler.GetAllGroups).Methods("GET", "OPTIONS")
|
||||
|
||||
326
management/server/http/policies.go
Normal file
326
management/server/http/policies.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/xid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/http/api"
|
||||
"github.com/netbirdio/netbird/management/server/http/util"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
)
|
||||
|
||||
// Policies is a handler that returns policy of the account
|
||||
type Policies struct {
|
||||
accountManager server.AccountManager
|
||||
claimsExtractor *jwtclaims.ClaimsExtractor
|
||||
}
|
||||
|
||||
// NewPoliciesHandler creates a new Policies handler
|
||||
func NewPoliciesHandler(accountManager server.AccountManager, authCfg AuthCfg) *Policies {
|
||||
return &Policies{
|
||||
accountManager: accountManager,
|
||||
claimsExtractor: jwtclaims.NewClaimsExtractor(
|
||||
jwtclaims.WithAudience(authCfg.Audience),
|
||||
jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllPolicies list for the account
|
||||
func (h *Policies) GetAllPolicies(w http.ResponseWriter, r *http.Request) {
|
||||
claims := h.claimsExtractor.FromRequestContext(r)
|
||||
account, user, err := h.accountManager.GetAccountFromToken(claims)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
accountPolicies, err := h.accountManager.ListPolicies(account.Id, user.Id)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(w, accountPolicies)
|
||||
}
|
||||
|
||||
// UpdatePolicy handles update to a policy identified by a given ID
|
||||
func (h *Policies) UpdatePolicy(w http.ResponseWriter, r *http.Request) {
|
||||
claims := h.claimsExtractor.FromRequestContext(r)
|
||||
account, user, err := h.accountManager.GetAccountFromToken(claims)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
policyID := vars["id"]
|
||||
if len(policyID) == 0 {
|
||||
util.WriteError(status.Errorf(status.InvalidArgument, "invalid policy ID"), w)
|
||||
return
|
||||
}
|
||||
|
||||
policyIdx := -1
|
||||
for i, policy := range account.Policies {
|
||||
if policy.ID == policyID {
|
||||
policyIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if policyIdx < 0 {
|
||||
util.WriteError(status.Errorf(status.NotFound, "couldn't find policy id %s", policyID), w)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PutApiPoliciesIdJSONRequestBody
|
||||
err = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
util.WriteError(status.Errorf(status.InvalidArgument, "policy name shouldn't be empty"), w)
|
||||
return
|
||||
}
|
||||
|
||||
policy := server.Policy{
|
||||
ID: policyID,
|
||||
Name: req.Name,
|
||||
Enabled: req.Enabled,
|
||||
Description: req.Description,
|
||||
Query: req.Query,
|
||||
}
|
||||
if req.Rules != nil {
|
||||
for _, r := range req.Rules {
|
||||
pr := server.PolicyRule{
|
||||
Destinations: groupMinimumsToStrings(account, r.Destinations),
|
||||
Sources: groupMinimumsToStrings(account, r.Sources),
|
||||
Name: r.Name,
|
||||
}
|
||||
pr.Enabled = r.Enabled
|
||||
if r.Description != nil {
|
||||
pr.Description = *r.Description
|
||||
}
|
||||
if r.Id != nil {
|
||||
pr.ID = *r.Id
|
||||
}
|
||||
switch r.Action {
|
||||
case api.PolicyRuleActionAccept:
|
||||
pr.Action = server.PolicyTrafficActionAccept
|
||||
case api.PolicyRuleActionDrop:
|
||||
pr.Action = server.PolicyTrafficActionDrop
|
||||
default:
|
||||
util.WriteError(status.Errorf(status.InvalidArgument, "unknown action type"), w)
|
||||
return
|
||||
}
|
||||
policy.Rules = append(policy.Rules, &pr)
|
||||
}
|
||||
}
|
||||
if err := policy.UpdateQueryFromRules(); err != nil {
|
||||
log.Errorf("failed to update policy query: %v", err)
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.accountManager.SavePolicy(account.Id, user.Id, &policy); err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(w, toPolicyResponse(account, &policy))
|
||||
}
|
||||
|
||||
// CreatePolicy handles policy creation request
|
||||
func (h *Policies) CreatePolicy(w http.ResponseWriter, r *http.Request) {
|
||||
claims := h.claimsExtractor.FromRequestContext(r)
|
||||
account, user, err := h.accountManager.GetAccountFromToken(claims)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PostApiPoliciesJSONRequestBody
|
||||
err = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
util.WriteError(status.Errorf(status.InvalidArgument, "policy name shouldn't be empty"), w)
|
||||
return
|
||||
}
|
||||
|
||||
policy := &server.Policy{
|
||||
ID: xid.New().String(),
|
||||
Name: req.Name,
|
||||
Enabled: req.Enabled,
|
||||
Description: req.Description,
|
||||
Query: req.Query,
|
||||
}
|
||||
|
||||
if req.Rules != nil {
|
||||
for _, r := range req.Rules {
|
||||
pr := server.PolicyRule{
|
||||
ID: xid.New().String(),
|
||||
Destinations: groupMinimumsToStrings(account, r.Destinations),
|
||||
Sources: groupMinimumsToStrings(account, r.Sources),
|
||||
Name: r.Name,
|
||||
}
|
||||
pr.Enabled = r.Enabled
|
||||
if r.Description != nil {
|
||||
pr.Description = *r.Description
|
||||
}
|
||||
switch r.Action {
|
||||
case api.PolicyRuleActionAccept:
|
||||
pr.Action = server.PolicyTrafficActionAccept
|
||||
case api.PolicyRuleActionDrop:
|
||||
pr.Action = server.PolicyTrafficActionDrop
|
||||
default:
|
||||
util.WriteError(status.Errorf(status.InvalidArgument, "unknown action type"), w)
|
||||
return
|
||||
}
|
||||
policy.Rules = append(policy.Rules, &pr)
|
||||
}
|
||||
}
|
||||
if err := policy.UpdateQueryFromRules(); err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.accountManager.SavePolicy(account.Id, user.Id, policy); err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(w, toPolicyResponse(account, policy))
|
||||
}
|
||||
|
||||
// DeletePolicy handles policy deletion request
|
||||
func (h *Policies) DeletePolicy(w http.ResponseWriter, r *http.Request) {
|
||||
claims := h.claimsExtractor.FromRequestContext(r)
|
||||
account, user, err := h.accountManager.GetAccountFromToken(claims)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
aID := account.Id
|
||||
|
||||
vars := mux.Vars(r)
|
||||
policyID := vars["id"]
|
||||
if len(policyID) == 0 {
|
||||
util.WriteError(status.Errorf(status.InvalidArgument, "invalid policy ID"), w)
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.accountManager.DeletePolicy(aID, policyID, user.Id); err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(w, "")
|
||||
}
|
||||
|
||||
// GetPolicy handles a group Get request identified by ID
|
||||
func (h *Policies) GetPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
claims := h.claimsExtractor.FromRequestContext(r)
|
||||
account, user, err := h.accountManager.GetAccountFromToken(claims)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
vars := mux.Vars(r)
|
||||
policyID := vars["id"]
|
||||
if len(policyID) == 0 {
|
||||
util.WriteError(status.Errorf(status.InvalidArgument, "invalid policy ID"), w)
|
||||
return
|
||||
}
|
||||
|
||||
policy, err := h.accountManager.GetPolicy(account.Id, policyID, user.Id)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(w, toPolicyResponse(account, policy))
|
||||
default:
|
||||
util.WriteError(status.Errorf(status.NotFound, "method not found"), w)
|
||||
}
|
||||
}
|
||||
|
||||
func toPolicyResponse(account *server.Account, policy *server.Policy) *api.Policy {
|
||||
cache := make(map[string]api.GroupMinimum)
|
||||
ap := &api.Policy{
|
||||
Id: policy.ID,
|
||||
Name: policy.Name,
|
||||
Description: policy.Description,
|
||||
Enabled: policy.Enabled,
|
||||
Query: policy.Query,
|
||||
}
|
||||
if len(policy.Rules) == 0 {
|
||||
return ap
|
||||
}
|
||||
|
||||
for _, r := range policy.Rules {
|
||||
rule := api.PolicyRule{
|
||||
Id: &r.ID,
|
||||
Name: r.Name,
|
||||
Enabled: r.Enabled,
|
||||
Description: &r.Description,
|
||||
}
|
||||
for _, gid := range r.Sources {
|
||||
_, ok := cache[gid]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
if group, ok := account.Groups[gid]; ok {
|
||||
minimum := api.GroupMinimum{
|
||||
Id: group.ID,
|
||||
Name: group.Name,
|
||||
PeersCount: len(group.Peers),
|
||||
}
|
||||
rule.Sources = append(rule.Sources, minimum)
|
||||
cache[gid] = minimum
|
||||
}
|
||||
}
|
||||
for _, gid := range r.Destinations {
|
||||
cachedMinimum, ok := cache[gid]
|
||||
if ok {
|
||||
rule.Destinations = append(rule.Destinations, cachedMinimum)
|
||||
continue
|
||||
}
|
||||
if group, ok := account.Groups[gid]; ok {
|
||||
minimum := api.GroupMinimum{
|
||||
Id: group.ID,
|
||||
Name: group.Name,
|
||||
PeersCount: len(group.Peers),
|
||||
}
|
||||
rule.Destinations = append(rule.Destinations, minimum)
|
||||
cache[gid] = minimum
|
||||
}
|
||||
}
|
||||
ap.Rules = append(ap.Rules, rule)
|
||||
}
|
||||
return ap
|
||||
}
|
||||
|
||||
func groupMinimumsToStrings(account *server.Account, gm []api.GroupMinimum) []string {
|
||||
result := make([]string, 0, len(gm))
|
||||
for _, gm := range gm {
|
||||
if _, ok := account.Groups[gm.Id]; ok {
|
||||
continue
|
||||
}
|
||||
result = append(result, gm.Id)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -40,14 +40,16 @@ func (h *RulesHandler) GetAllRules(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
accountRules, err := h.accountManager.ListRules(account.Id, user.Id)
|
||||
accountPolicies, err := h.accountManager.ListPolicies(account.Id, user.Id)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
rules := []*api.Rule{}
|
||||
for _, r := range accountRules {
|
||||
rules = append(rules, toRuleResponse(account, r))
|
||||
for _, policy := range accountPolicies {
|
||||
for _, r := range policy.Rules {
|
||||
rules = append(rules, toRuleResponse(account, r.ToRule()))
|
||||
}
|
||||
}
|
||||
|
||||
util.WriteJSONObject(w, rules)
|
||||
@@ -69,9 +71,9 @@ func (h *RulesHandler) UpdateRule(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
_, ok := account.Rules[ruleID]
|
||||
if !ok {
|
||||
util.WriteError(status.Errorf(status.NotFound, "couldn't find rule id %s", ruleID), w)
|
||||
policy, err := h.accountManager.GetPolicy(account.Id, ruleID, user.Id)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -96,173 +98,40 @@ func (h *RulesHandler) UpdateRule(w http.ResponseWriter, r *http.Request) {
|
||||
reqDestinations = *req.Destinations
|
||||
}
|
||||
|
||||
rule := server.Rule{
|
||||
ID: ruleID,
|
||||
Name: req.Name,
|
||||
Source: reqSources,
|
||||
Destination: reqDestinations,
|
||||
Disabled: req.Disabled,
|
||||
Description: req.Description,
|
||||
if len(policy.Rules) != 1 {
|
||||
util.WriteError(status.Errorf(status.Internal, "policy should contain exactly one rule"), w)
|
||||
return
|
||||
}
|
||||
|
||||
policy.Name = req.Name
|
||||
policy.Description = req.Description
|
||||
policy.Enabled = !req.Disabled
|
||||
policy.Rules[0].ID = ruleID
|
||||
policy.Rules[0].Name = req.Name
|
||||
policy.Rules[0].Sources = reqSources
|
||||
policy.Rules[0].Destinations = reqDestinations
|
||||
policy.Rules[0].Enabled = !req.Disabled
|
||||
policy.Rules[0].Description = req.Description
|
||||
if err := policy.UpdateQueryFromRules(); err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Flow {
|
||||
case server.TrafficFlowBidirectString:
|
||||
rule.Flow = server.TrafficFlowBidirect
|
||||
policy.Rules[0].Action = server.PolicyTrafficActionAccept
|
||||
default:
|
||||
util.WriteError(status.Errorf(status.InvalidArgument, "unknown flow type"), w)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.accountManager.SaveRule(account.Id, user.Id, &rule)
|
||||
err = h.accountManager.SavePolicy(account.Id, user.Id, policy)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
resp := toRuleResponse(account, &rule)
|
||||
|
||||
util.WriteJSONObject(w, &resp)
|
||||
}
|
||||
|
||||
// PatchRule handles patch updates to a rule identified by a given ID
|
||||
func (h *RulesHandler) PatchRule(w http.ResponseWriter, r *http.Request) {
|
||||
claims := h.claimsExtractor.FromRequestContext(r)
|
||||
account, _, err := h.accountManager.GetAccountFromToken(claims)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
ruleID := vars["id"]
|
||||
if len(ruleID) == 0 {
|
||||
util.WriteError(status.Errorf(status.InvalidArgument, "invalid rule ID"), w)
|
||||
return
|
||||
}
|
||||
|
||||
_, ok := account.Rules[ruleID]
|
||||
if !ok {
|
||||
util.WriteError(status.Errorf(status.NotFound, "couldn't find rule ID %s", ruleID), w)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PatchApiRulesIdJSONRequestBody
|
||||
err = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req) == 0 {
|
||||
util.WriteError(status.Errorf(status.InvalidArgument, "no patch instruction received"), w)
|
||||
return
|
||||
}
|
||||
|
||||
var operations []server.RuleUpdateOperation
|
||||
|
||||
for _, patch := range req {
|
||||
switch patch.Path {
|
||||
case api.RulePatchOperationPathName:
|
||||
if patch.Op != api.RulePatchOperationOpReplace {
|
||||
util.WriteError(status.Errorf(status.InvalidArgument,
|
||||
"name field only accepts replace operation, got %s", patch.Op), w)
|
||||
return
|
||||
}
|
||||
if len(patch.Value) == 0 || patch.Value[0] == "" {
|
||||
util.WriteError(status.Errorf(status.InvalidArgument, "rule name shouldn't be empty"), w)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.UpdateRuleName,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationPathDescription:
|
||||
if patch.Op != api.RulePatchOperationOpReplace {
|
||||
util.WriteError(status.Errorf(status.InvalidArgument,
|
||||
"description field only accepts replace operation, got %s", patch.Op), w)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.UpdateRuleDescription,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationPathFlow:
|
||||
if patch.Op != api.RulePatchOperationOpReplace {
|
||||
util.WriteError(status.Errorf(status.InvalidArgument,
|
||||
"flow field only accepts replace operation, got %s", patch.Op), w)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.UpdateRuleFlow,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationPathDisabled:
|
||||
if patch.Op != api.RulePatchOperationOpReplace {
|
||||
util.WriteError(status.Errorf(status.InvalidArgument,
|
||||
"disabled field only accepts replace operation, got %s", patch.Op), w)
|
||||
return
|
||||
}
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.UpdateRuleStatus,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationPathSources:
|
||||
switch patch.Op {
|
||||
case api.RulePatchOperationOpReplace:
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.UpdateSourceGroups,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationOpRemove:
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.RemoveGroupsFromSource,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationOpAdd:
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.InsertGroupsToSource,
|
||||
Values: patch.Value,
|
||||
})
|
||||
default:
|
||||
util.WriteError(status.Errorf(status.InvalidArgument,
|
||||
"invalid operation \"%s\" on Source field", patch.Op), w)
|
||||
return
|
||||
}
|
||||
case api.RulePatchOperationPathDestinations:
|
||||
switch patch.Op {
|
||||
case api.RulePatchOperationOpReplace:
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.UpdateDestinationGroups,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationOpRemove:
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.RemoveGroupsFromDestination,
|
||||
Values: patch.Value,
|
||||
})
|
||||
case api.RulePatchOperationOpAdd:
|
||||
operations = append(operations, server.RuleUpdateOperation{
|
||||
Type: server.InsertGroupsToDestination,
|
||||
Values: patch.Value,
|
||||
})
|
||||
default:
|
||||
util.WriteError(status.Errorf(status.InvalidArgument,
|
||||
"invalid operation \"%s\" on Destination field", patch.Op), w)
|
||||
return
|
||||
}
|
||||
default:
|
||||
util.WriteError(status.Errorf(status.InvalidArgument, "invalid patch path"), w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rule, err := h.accountManager.UpdateRule(account.Id, ruleID, operations)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
resp := toRuleResponse(account, rule)
|
||||
resp := toRuleResponse(account, policy.Rules[0].ToRule())
|
||||
|
||||
util.WriteJSONObject(w, &resp)
|
||||
}
|
||||
@@ -315,7 +184,12 @@ func (h *RulesHandler) CreateRule(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = h.accountManager.SaveRule(account.Id, user.Id, &rule)
|
||||
policy, err := server.RuleToPolicy(&rule)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
err = h.accountManager.SavePolicy(account.Id, user.Id, policy)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
@@ -342,7 +216,7 @@ func (h *RulesHandler) DeleteRule(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = h.accountManager.DeleteRule(aID, rID, user.Id)
|
||||
err = h.accountManager.DeletePolicy(aID, rID, user.Id)
|
||||
if err != nil {
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
@@ -368,13 +242,13 @@ func (h *RulesHandler) GetRule(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
rule, err := h.accountManager.GetRule(account.Id, ruleID, user.Id)
|
||||
policy, err := h.accountManager.GetPolicy(account.Id, ruleID, user.Id)
|
||||
if err != nil {
|
||||
util.WriteError(status.Errorf(status.NotFound, "rule not found"), w)
|
||||
util.WriteError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(w, toRuleResponse(account, rule))
|
||||
util.WriteJSONObject(w, toRuleResponse(account, policy.Rules[0].ToRule()))
|
||||
default:
|
||||
util.WriteError(status.Errorf(status.NotFound, "method not found"), w)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/http/api"
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
@@ -23,8 +24,32 @@ import (
|
||||
)
|
||||
|
||||
func initRulesTestData(rules ...*server.Rule) *RulesHandler {
|
||||
testPolicies := make(map[string]*server.Policy, len(rules))
|
||||
for _, rule := range rules {
|
||||
policy, err := server.RuleToPolicy(rule)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := policy.UpdateQueryFromRules(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
testPolicies[policy.ID] = policy
|
||||
}
|
||||
return &RulesHandler{
|
||||
accountManager: &mock_server.MockAccountManager{
|
||||
GetPolicyFunc: func(_, policyID, _ string) (*server.Policy, error) {
|
||||
policy, ok := testPolicies[policyID]
|
||||
if !ok {
|
||||
return nil, status.Errorf(status.NotFound, "policy not found")
|
||||
}
|
||||
return policy, nil
|
||||
},
|
||||
SavePolicyFunc: func(_, _ string, policy *server.Policy) error {
|
||||
if !strings.HasPrefix(policy.ID, "id-") {
|
||||
policy.ID = "id-was-set"
|
||||
}
|
||||
return nil
|
||||
},
|
||||
SaveRuleFunc: func(_, _ string, rule *server.Rule) error {
|
||||
if !strings.HasPrefix(rule.ID, "id-") {
|
||||
rule.ID = "id-was-set"
|
||||
@@ -43,32 +68,6 @@ func initRulesTestData(rules ...*server.Rule) *RulesHandler {
|
||||
Flow: server.TrafficFlowBidirect,
|
||||
}, nil
|
||||
},
|
||||
UpdateRuleFunc: func(_ string, ruleID string, operations []server.RuleUpdateOperation) (*server.Rule, error) {
|
||||
var rule server.Rule
|
||||
rule.ID = ruleID
|
||||
for _, operation := range operations {
|
||||
switch operation.Type {
|
||||
case server.UpdateRuleName:
|
||||
rule.Name = operation.Values[0]
|
||||
case server.UpdateRuleDescription:
|
||||
rule.Description = operation.Values[0]
|
||||
case server.UpdateRuleFlow:
|
||||
if server.TrafficFlowBidirectString == operation.Values[0] {
|
||||
rule.Flow = server.TrafficFlowBidirect
|
||||
} else {
|
||||
rule.Flow = 100
|
||||
}
|
||||
case server.UpdateSourceGroups, server.InsertGroupsToSource:
|
||||
rule.Source = operation.Values
|
||||
case server.UpdateDestinationGroups, server.InsertGroupsToDestination:
|
||||
rule.Destination = operation.Values
|
||||
case server.RemoveGroupsFromSource, server.RemoveGroupsFromDestination:
|
||||
default:
|
||||
return nil, fmt.Errorf("no operation")
|
||||
}
|
||||
}
|
||||
return &rule, nil
|
||||
},
|
||||
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
|
||||
user := server.NewAdminUser("test_user")
|
||||
return &server.Account{
|
||||
@@ -221,58 +220,13 @@ func TestRulesWriteRule(t *testing.T) {
|
||||
[]byte(`{"Name":"","Flow":"bidirect"}`)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
{
|
||||
name: "Write Rule PATCH Name OK",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/rules/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`[{"op":"replace","path":"name","value":["Default POSTed Rule"]}]`)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRule: &api.Rule{
|
||||
Id: "id-existed",
|
||||
Name: "Default POSTed Rule",
|
||||
Flow: server.TrafficFlowBidirectString,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Write Rule PATCH Invalid Name OP",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/rules/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`[{"op":"insert","path":"name","value":[""]}]`)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "Write Rule PATCH Invalid Name",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/rules/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`[{"op":"replace","path":"name","value":[]}]`)),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "Write Rule PATCH Sources OK",
|
||||
requestType: http.MethodPatch,
|
||||
requestPath: "/api/rules/id-existed",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(`[{"op":"replace","path":"sources","value":["G","F"]}]`)),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
expectedRule: &api.Rule{
|
||||
Id: "id-existed",
|
||||
Flow: server.TrafficFlowBidirectString,
|
||||
Sources: []api.GroupMinimum{
|
||||
{Id: "G"},
|
||||
{Id: "F"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p := initRulesTestData()
|
||||
p := initRulesTestData(&server.Rule{
|
||||
ID: "id-existed",
|
||||
Name: "Default POSTed Rule",
|
||||
Flow: server.TrafficFlowBidirect,
|
||||
})
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -282,7 +236,6 @@ func TestRulesWriteRule(t *testing.T) {
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/api/rules", p.CreateRule).Methods("POST")
|
||||
router.HandleFunc("/api/rules/{id}", p.UpdateRule).Methods("PUT")
|
||||
router.HandleFunc("/api/rules/{id}", p.PatchRule).Methods("PATCH")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
@@ -307,6 +260,7 @@ func TestRulesWriteRule(t *testing.T) {
|
||||
if err = json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.expectedRule.Id = got.Id
|
||||
|
||||
assert.Equal(t, got, tc.expectedRule)
|
||||
})
|
||||
|
||||
@@ -75,7 +75,7 @@ func getServerKey(client mgmtProto.ManagementServiceClient) (*wgtypes.Key, error
|
||||
|
||||
func Test_SyncProtocol(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := util.CopyFileContents("testdata/store.json", filepath.Join(dir, "store.json"))
|
||||
err := util.CopyFileContents("testdata/store_with_expired_peers.json", filepath.Join(dir, "store.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -117,6 +117,7 @@ func Test_SyncProtocol(t *testing.T) {
|
||||
|
||||
defer clientConn.Close()
|
||||
|
||||
// there are two peers already in the store, add two more
|
||||
peers, err := registerPeers(2, client)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -129,7 +130,7 @@ func Test_SyncProtocol(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
// take the first registered peer as a base for the test
|
||||
// take the first registered peer as a base for the test. Total four.
|
||||
key := *peers[0]
|
||||
|
||||
message, err := encryption.EncryptMessage(*serverKey, key, &mgmtProto.SyncRequest{})
|
||||
@@ -242,8 +243,18 @@ func Test_SyncProtocol(t *testing.T) {
|
||||
t.Fatal("expecting SyncResponse to have non-nil NetworkMap")
|
||||
}
|
||||
|
||||
if len(networkMap.GetRemotePeers()) != 1 {
|
||||
t.Fatal("expecting SyncResponse to have NetworkMap with 1 remote peer")
|
||||
if len(networkMap.GetRemotePeers()) != 3 {
|
||||
t.Fatalf("expecting SyncResponse to have NetworkMap with 3 remote peers, got %d", len(networkMap.GetRemotePeers()))
|
||||
}
|
||||
|
||||
// expired peers come separately.
|
||||
if len(networkMap.GetOfflinePeers()) != 1 {
|
||||
t.Fatal("expecting SyncResponse to have NetworkMap with 1 offline peer")
|
||||
}
|
||||
|
||||
expiredPeerPubKey := "RlSy2vzoG2HyMBTUImXOiVhCBiiBa5qD5xzMxkiFDW4="
|
||||
if networkMap.GetOfflinePeers()[0].WgPubKey != expiredPeerPubKey {
|
||||
t.Fatalf("expecting SyncResponse to have NetworkMap with 1 offline peer with a key %s", expiredPeerPubKey)
|
||||
}
|
||||
|
||||
if networkMap.GetPeerConfig() == nil {
|
||||
|
||||
@@ -2,7 +2,6 @@ package server_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
@@ -11,6 +10,8 @@ import (
|
||||
sync2 "sync"
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
|
||||
server "github.com/netbirdio/netbird/management/server"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
@@ -29,6 +30,7 @@ import (
|
||||
|
||||
const (
|
||||
ValidSetupKey = "A2C8E62B-38F5-4553-B31E-DD66C696CEBB"
|
||||
AccountKey = "bf1c8084-ba50-4ce7-9439-34653001fc3b"
|
||||
)
|
||||
|
||||
var _ = Describe("Management service", func() {
|
||||
@@ -127,6 +129,7 @@ var _ = Describe("Management service", func() {
|
||||
actualTURN := resp.WiretrusteeConfig.Turns[0]
|
||||
Expect(len(actualTURN.User) > 0).To(BeTrue())
|
||||
Expect(actualTURN.HostConfig).To(BeEquivalentTo(expectedTRUNHost))
|
||||
Expect(len(resp.NetworkMap.OfflinePeers) == 0).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -240,7 +243,8 @@ var _ = Describe("Management service", func() {
|
||||
Context("with an invalid setup key", func() {
|
||||
Specify("an error is returned", func() {
|
||||
key, _ := wgtypes.GenerateKey()
|
||||
message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{SetupKey: "invalid setup key"})
|
||||
message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{SetupKey: "invalid setup key",
|
||||
Meta: &mgmtProto.PeerSystemMeta{}})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
resp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{
|
||||
@@ -269,7 +273,7 @@ var _ = Describe("Management service", func() {
|
||||
Expect(regResp).NotTo(BeNil())
|
||||
|
||||
// just login without registration
|
||||
message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{})
|
||||
message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{Meta: &mgmtProto.PeerSystemMeta{}})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
loginResp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{
|
||||
WgPubKey: key.PublicKey().String(),
|
||||
|
||||
@@ -5,16 +5,18 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/netbirdio/netbird/client/system"
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
nbversion "github.com/netbirdio/netbird/version"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -176,7 +178,7 @@ func (w *Worker) generateProperties() properties {
|
||||
osUIClients = make(map[string]int)
|
||||
uptime = time.Since(w.startupTime).Seconds()
|
||||
connections := w.connManager.GetAllConnectedPeers()
|
||||
version = system.NetbirdVersion()
|
||||
version = nbversion.NetbirdVersion()
|
||||
|
||||
for _, account := range w.dataSource.GetAllAccounts() {
|
||||
accounts++
|
||||
|
||||
@@ -25,12 +25,11 @@ type MockAccountManager struct {
|
||||
GetPeerByKeyFunc func(peerKey string) (*server.Peer, error)
|
||||
GetPeersFunc func(accountID, userID string) ([]*server.Peer, error)
|
||||
MarkPeerConnectedFunc func(peerKey string, connected bool) error
|
||||
MarkPeerLoginExpiredFunc func(peerPubKey string, loginExpired bool) error
|
||||
DeletePeerFunc func(accountID, peerKey, userID string) (*server.Peer, error)
|
||||
GetPeerByIPFunc func(accountId string, peerIP string) (*server.Peer, error)
|
||||
GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error)
|
||||
GetPeerNetworkFunc func(peerKey string) (*server.Network, error)
|
||||
AddPeerFunc func(setupKey string, userId string, peer *server.Peer) (*server.Peer, error)
|
||||
AddPeerFunc func(setupKey string, userId string, peer *server.Peer) (*server.Peer, *server.NetworkMap, error)
|
||||
GetGroupFunc func(accountID, groupID string) (*server.Group, error)
|
||||
SaveGroupFunc func(accountID, userID string, group *server.Group) error
|
||||
UpdateGroupFunc func(accountID string, groupID string, operations []server.GroupUpdateOperation) (*server.Group, error)
|
||||
@@ -41,9 +40,12 @@ type MockAccountManager struct {
|
||||
GroupListPeersFunc func(accountID, groupID string) ([]*server.Peer, error)
|
||||
GetRuleFunc func(accountID, ruleID, userID string) (*server.Rule, error)
|
||||
SaveRuleFunc func(accountID, userID string, rule *server.Rule) error
|
||||
UpdateRuleFunc func(accountID string, ruleID string, operations []server.RuleUpdateOperation) (*server.Rule, error)
|
||||
DeleteRuleFunc func(accountID, ruleID, userID string) error
|
||||
ListRulesFunc func(accountID, userID string) ([]*server.Rule, error)
|
||||
GetPolicyFunc func(accountID, policyID, userID string) (*server.Policy, error)
|
||||
SavePolicyFunc func(accountID, userID string, policy *server.Policy) error
|
||||
DeletePolicyFunc func(accountID, policyID, userID string) error
|
||||
ListPoliciesFunc func(accountID, userID string) ([]*server.Policy, error)
|
||||
GetUsersFromAccountFunc func(accountID, userID string) ([]*server.UserInfo, error)
|
||||
UpdatePeerMetaFunc func(peerID string, meta server.PeerSystemMeta) error
|
||||
UpdatePeerSSHKeyFunc func(peerID string, sshKey string) error
|
||||
@@ -70,9 +72,9 @@ type MockAccountManager struct {
|
||||
GetDNSSettingsFunc func(accountID, userID string) (*server.DNSSettings, error)
|
||||
SaveDNSSettingsFunc func(accountID, userID string, dnsSettingsToSave *server.DNSSettings) error
|
||||
GetPeerFunc func(accountID, peerID, userID string) (*server.Peer, error)
|
||||
GetAccountByPeerIDFunc func(peerID string) (*server.Account, error)
|
||||
UpdatePeerLastLoginFunc func(peerID string) error
|
||||
UpdateAccountSettingsFunc func(accountID, userID string, newSettings *server.Settings) (*server.Account, error)
|
||||
LoginPeerFunc func(login server.PeerLogin) (*server.Peer, *server.NetworkMap, error)
|
||||
SyncPeerFunc func(sync server.PeerSync) (*server.Peer, *server.NetworkMap, error)
|
||||
}
|
||||
|
||||
// GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface
|
||||
@@ -165,14 +167,6 @@ func (am *MockAccountManager) MarkPeerConnected(peerKey string, connected bool)
|
||||
return status.Errorf(codes.Unimplemented, "method MarkPeerConnected is not implemented")
|
||||
}
|
||||
|
||||
// MarkPeerLoginExpired mock implementation of MarkPeerLoginExpired from server.AccountManager interface
|
||||
func (am *MockAccountManager) MarkPeerLoginExpired(peerPubKey string, loginExpired bool) error {
|
||||
if am.MarkPeerLoginExpiredFunc != nil {
|
||||
return am.MarkPeerLoginExpiredFunc(peerPubKey, loginExpired)
|
||||
}
|
||||
return status.Errorf(codes.Unimplemented, "method MarkPeerLoginExpired is not implemented")
|
||||
}
|
||||
|
||||
// GetPeerByIP mock implementation of GetPeerByIP from server.AccountManager interface
|
||||
func (am *MockAccountManager) GetPeerByIP(accountId string, peerIP string) (*server.Peer, error) {
|
||||
if am.GetPeerByIPFunc != nil {
|
||||
@@ -202,11 +196,11 @@ func (am *MockAccountManager) AddPeer(
|
||||
setupKey string,
|
||||
userId string,
|
||||
peer *server.Peer,
|
||||
) (*server.Peer, error) {
|
||||
) (*server.Peer, *server.NetworkMap, error) {
|
||||
if am.AddPeerFunc != nil {
|
||||
return am.AddPeerFunc(setupKey, userId, peer)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AddPeer is not implemented")
|
||||
return nil, nil, status.Errorf(codes.Unimplemented, "method AddPeer is not implemented")
|
||||
}
|
||||
|
||||
// GetGroup mock implementation of GetGroup from server.AccountManager interface
|
||||
@@ -289,14 +283,6 @@ func (am *MockAccountManager) SaveRule(accountID, userID string, rule *server.Ru
|
||||
return status.Errorf(codes.Unimplemented, "method SaveRule is not implemented")
|
||||
}
|
||||
|
||||
// UpdateRule mock implementation of UpdateRule from server.AccountManager interface
|
||||
func (am *MockAccountManager) UpdateRule(accountID string, ruleID string, operations []server.RuleUpdateOperation) (*server.Rule, error) {
|
||||
if am.UpdateRuleFunc != nil {
|
||||
return am.UpdateRuleFunc(accountID, ruleID, operations)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateRule not implemented")
|
||||
}
|
||||
|
||||
// DeleteRule mock implementation of DeleteRule from server.AccountManager interface
|
||||
func (am *MockAccountManager) DeleteRule(accountID, ruleID, userID string) error {
|
||||
if am.DeleteRuleFunc != nil {
|
||||
@@ -313,6 +299,38 @@ func (am *MockAccountManager) ListRules(accountID, userID string) ([]*server.Rul
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRules is not implemented")
|
||||
}
|
||||
|
||||
// GetPolicy mock implementation of GetPolicy from server.AccountManager interface
|
||||
func (am *MockAccountManager) GetPolicy(accountID, policyID, userID string) (*server.Policy, error) {
|
||||
if am.GetPolicyFunc != nil {
|
||||
return am.GetPolicyFunc(accountID, policyID, userID)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetPolicy is not implemented")
|
||||
}
|
||||
|
||||
// SavePolicy mock implementation of SavePolicy from server.AccountManager interface
|
||||
func (am *MockAccountManager) SavePolicy(accountID, userID string, policy *server.Policy) error {
|
||||
if am.SavePolicyFunc != nil {
|
||||
return am.SavePolicyFunc(accountID, userID, policy)
|
||||
}
|
||||
return status.Errorf(codes.Unimplemented, "method SavePolicy is not implemented")
|
||||
}
|
||||
|
||||
// DeletePolicy mock implementation of DeletePolicy from server.AccountManager interface
|
||||
func (am *MockAccountManager) DeletePolicy(accountID, policyID, userID string) error {
|
||||
if am.DeletePolicyFunc != nil {
|
||||
return am.DeletePolicyFunc(accountID, policyID, userID)
|
||||
}
|
||||
return status.Errorf(codes.Unimplemented, "method DeletePolicy is not implemented")
|
||||
}
|
||||
|
||||
// ListPolicies mock implementation of ListPolicies from server.AccountManager interface
|
||||
func (am *MockAccountManager) ListPolicies(accountID, userID string) ([]*server.Policy, error) {
|
||||
if am.ListPoliciesFunc != nil {
|
||||
return am.ListPoliciesFunc(accountID, userID)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListPolicies is not implemented")
|
||||
}
|
||||
|
||||
// UpdatePeerMeta mock implementation of UpdatePeerMeta from server.AccountManager interface
|
||||
func (am *MockAccountManager) UpdatePeerMeta(peerID string, meta server.PeerSystemMeta) error {
|
||||
if am.UpdatePeerMetaFunc != nil {
|
||||
@@ -486,7 +504,8 @@ func (am *MockAccountManager) CreateUser(accountID, userID string, invite *serve
|
||||
|
||||
// GetAccountFromToken mocks GetAccountFromToken of the AccountManager interface
|
||||
func (am *MockAccountManager) GetAccountFromToken(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User,
|
||||
error) {
|
||||
error,
|
||||
) {
|
||||
if am.GetAccountFromTokenFunc != nil {
|
||||
return am.GetAccountFromTokenFunc(claims)
|
||||
}
|
||||
@@ -541,22 +560,6 @@ func (am *MockAccountManager) GetPeer(accountID, peerID, userID string) (*server
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetPeer is not implemented")
|
||||
}
|
||||
|
||||
// GetAccountByPeerID mocks GetAccountByPeerID of the AccountManager interface
|
||||
func (am *MockAccountManager) GetAccountByPeerID(peerID string) (*server.Account, error) {
|
||||
if am.GetAccountByPeerIDFunc != nil {
|
||||
return am.GetAccountByPeerIDFunc(peerID)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetAccountByPeerID is not implemented")
|
||||
}
|
||||
|
||||
// UpdatePeerLastLogin mocks UpdatePeerLastLogin of the AccountManager interface
|
||||
func (am *MockAccountManager) UpdatePeerLastLogin(peerID string) error {
|
||||
if am.UpdatePeerLastLoginFunc != nil {
|
||||
return am.UpdatePeerLastLoginFunc(peerID)
|
||||
}
|
||||
return status.Errorf(codes.Unimplemented, "method UpdatePeerLastLogin is not implemented")
|
||||
}
|
||||
|
||||
// UpdateAccountSettings mocks UpdateAccountSettings of the AccountManager interface
|
||||
func (am *MockAccountManager) UpdateAccountSettings(accountID, userID string, newSettings *server.Settings) (*server.Account, error) {
|
||||
if am.UpdateAccountSettingsFunc != nil {
|
||||
@@ -564,3 +567,19 @@ func (am *MockAccountManager) UpdateAccountSettings(accountID, userID string, ne
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateAccountSettings is not implemented")
|
||||
}
|
||||
|
||||
// LoginPeer mocks LoginPeer of the AccountManager interface
|
||||
func (am *MockAccountManager) LoginPeer(login server.PeerLogin) (*server.Peer, *server.NetworkMap, error) {
|
||||
if am.LoginPeerFunc != nil {
|
||||
return am.LoginPeerFunc(login)
|
||||
}
|
||||
return nil, nil, status.Errorf(codes.Unimplemented, "method LoginPeer is not implemented")
|
||||
}
|
||||
|
||||
// SyncPeer mocks SyncPeer of the AccountManager interface
|
||||
func (am *MockAccountManager) SyncPeer(sync server.PeerSync) (*server.Peer, *server.NetworkMap, error) {
|
||||
if am.SyncPeerFunc != nil {
|
||||
return am.SyncPeerFunc(sync)
|
||||
}
|
||||
return nil, nil, status.Errorf(codes.Unimplemented, "method SyncPeer is not implemented")
|
||||
}
|
||||
|
||||
@@ -1147,11 +1147,11 @@ func initTestNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = am.AddPeer("", userID, peer1)
|
||||
_, _, err = am.AddPeer("", userID, peer1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = am.AddPeer("", userID, peer2)
|
||||
_, _, err = am.AddPeer("", userID, peer2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -23,10 +23,11 @@ const (
|
||||
)
|
||||
|
||||
type NetworkMap struct {
|
||||
Peers []*Peer
|
||||
Network *Network
|
||||
Routes []*route.Route
|
||||
DNSConfig nbdns.Config
|
||||
Peers []*Peer
|
||||
Network *Network
|
||||
Routes []*route.Route
|
||||
DNSConfig nbdns.Config
|
||||
OfflinePeers []*Peer
|
||||
}
|
||||
|
||||
type Network struct {
|
||||
|
||||
@@ -2,14 +2,14 @@ package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
"github.com/rs/xid"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
"github.com/rs/xid"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/proto"
|
||||
@@ -36,6 +36,26 @@ type PeerStatus struct {
|
||||
LoginExpired bool
|
||||
}
|
||||
|
||||
// PeerSync used as a data object between the gRPC API and AccountManager on Sync request.
|
||||
type PeerSync struct {
|
||||
// WireGuardPubKey is a peers WireGuard public key
|
||||
WireGuardPubKey string
|
||||
}
|
||||
|
||||
// PeerLogin used as a data object between the gRPC API and AccountManager on Login request.
|
||||
type PeerLogin struct {
|
||||
// WireGuardPubKey is a peers WireGuard public key
|
||||
WireGuardPubKey string
|
||||
// SSHKey is a peer's ssh key. Can be empty (e.g., old version do not provide it, or this feature is disabled)
|
||||
SSHKey string
|
||||
// Meta is the system information passed by peer, must be always present.
|
||||
Meta PeerSystemMeta
|
||||
// UserID indicates that JWT was used to log in, and it was valid. Can be empty when SetupKey is used or auth is not required.
|
||||
UserID string
|
||||
// SetupKey references to a server.SetupKey to log in. Can be empty when UserID is used or auth is not required.
|
||||
SetupKey string
|
||||
}
|
||||
|
||||
// Peer represents a machine connected to the network.
|
||||
// The Peer is a WireGuard peer identified by a public key
|
||||
type Peer struct {
|
||||
@@ -93,6 +113,15 @@ func (p *Peer) Copy() *Peer {
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateMeta updates peer's system meta data
|
||||
func (p *Peer) UpdateMeta(meta PeerSystemMeta) {
|
||||
// Avoid overwriting UIVersion if the update was triggered sole by the CLI client
|
||||
if meta.UIVersion == "" {
|
||||
meta.UIVersion = p.Meta.UIVersion
|
||||
}
|
||||
p.Meta = meta
|
||||
}
|
||||
|
||||
// MarkLoginExpired marks peer's status expired or not
|
||||
func (p *Peer) MarkLoginExpired(expired bool) {
|
||||
newStatus := p.Status.Copy()
|
||||
@@ -108,11 +137,15 @@ func (p *Peer) MarkLoginExpired(expired bool) {
|
||||
// Return true if a login has expired, false otherwise, and time left to expiration (negative when expired).
|
||||
// Login expiration can be disabled/enabled on a Peer level via Peer.LoginExpirationEnabled property.
|
||||
// Login expiration can also be disabled/enabled globally on the Account level via Settings.PeerLoginExpirationEnabled.
|
||||
// Only peers added by interactive SSO login can be expired.
|
||||
func (p *Peer) LoginExpired(expiresIn time.Duration) (bool, time.Duration) {
|
||||
if !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled {
|
||||
return false, 0
|
||||
}
|
||||
expiresAt := p.LastLogin.Add(expiresIn)
|
||||
now := time.Now()
|
||||
timeLeft := expiresAt.Sub(now)
|
||||
return p.LoginExpirationEnabled && (timeLeft <= 0), timeLeft
|
||||
return timeLeft <= 0, timeLeft
|
||||
}
|
||||
|
||||
// FQDN returns peers FQDN combined of the peer's DNS label and the system's DNS domain
|
||||
@@ -139,7 +172,6 @@ func (p *PeerStatus) Copy() *PeerStatus {
|
||||
|
||||
// GetPeerByKey looks up peer by its public WireGuard key
|
||||
func (am *DefaultAccountManager) GetPeerByKey(peerPubKey string) (*Peer, error) {
|
||||
|
||||
account, err := am.Store.GetAccountByPeerPubKey(peerPubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -151,7 +183,6 @@ func (am *DefaultAccountManager) GetPeerByKey(peerPubKey string) (*Peer, error)
|
||||
// GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if
|
||||
// the current user is not an admin.
|
||||
func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*Peer, error) {
|
||||
|
||||
account, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -176,7 +207,8 @@ func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*Peer, er
|
||||
|
||||
// fetch all the peers that have access to the user's peers
|
||||
for _, peer := range peers {
|
||||
aclPeers := account.getPeersByACL(peer.ID)
|
||||
// TODO: use firewall rules
|
||||
aclPeers, _ := account.getPeersByPolicy(peer.ID)
|
||||
for _, p := range aclPeers {
|
||||
peersMap[p.ID] = p
|
||||
}
|
||||
@@ -190,40 +222,8 @@ func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*Peer, er
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
// MarkPeerLoginExpired when peer login has expired
|
||||
func (am *DefaultAccountManager) MarkPeerLoginExpired(peerPubKey string, loginExpired bool) error {
|
||||
account, err := am.Store.GetAccountByPeerPubKey(peerPubKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unlock := am.Store.AcquireAccountLock(account.Id)
|
||||
defer unlock()
|
||||
|
||||
// ensure that we consider modification happened meanwhile (because we were outside the account lock when we fetched the account)
|
||||
account, err = am.Store.GetAccount(account.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peer, err := account.FindPeerByPubKey(peerPubKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peer.MarkLoginExpired(loginExpired)
|
||||
account.UpdatePeer(peer)
|
||||
|
||||
err = am.Store.SavePeerStatus(account.Id, peer.ID, *peer.Status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkPeerConnected marks peer as connected (true) or disconnected (false)
|
||||
func (am *DefaultAccountManager) MarkPeerConnected(peerPubKey string, connected bool) error {
|
||||
|
||||
account, err := am.Store.GetAccountByPeerPubKey(peerPubKey)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -265,7 +265,7 @@ func (am *DefaultAccountManager) MarkPeerConnected(peerPubKey string, connected
|
||||
|
||||
if oldStatus.LoginExpired {
|
||||
// we need to update other peers because when peer login expires all other peers are notified to disconnect from
|
||||
//the expired one. Here we notify them that connection is now allowed again.
|
||||
// the expired one. Here we notify them that connection is now allowed again.
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -277,7 +277,6 @@ func (am *DefaultAccountManager) MarkPeerConnected(peerPubKey string, connected
|
||||
|
||||
// UpdatePeer updates peer. Only Peer.Name, Peer.SSHEnabled, and Peer.LoginExpirationEnabled can be updated.
|
||||
func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *Peer) (*Peer, error) {
|
||||
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
@@ -351,7 +350,6 @@ func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *Pe
|
||||
|
||||
// DeletePeer removes peer from the account by its IP
|
||||
func (am *DefaultAccountManager) DeletePeer(accountID, peerID, userID string) (*Peer, error) {
|
||||
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
@@ -401,7 +399,6 @@ func (am *DefaultAccountManager) DeletePeer(accountID, peerID, userID string) (*
|
||||
|
||||
// GetPeerByIP returns peer by its IP
|
||||
func (am *DefaultAccountManager) GetPeerByIP(accountID string, peerIP string) (*Peer, error) {
|
||||
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
@@ -421,7 +418,6 @@ func (am *DefaultAccountManager) GetPeerByIP(accountID string, peerIP string) (*
|
||||
|
||||
// GetNetworkMap returns Network map for a given peer (omits original peer from the Peers result)
|
||||
func (am *DefaultAccountManager) GetNetworkMap(peerID string) (*NetworkMap, error) {
|
||||
|
||||
account, err := am.Store.GetAccountByPeerID(peerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -431,37 +427,11 @@ func (am *DefaultAccountManager) GetNetworkMap(peerID string) (*NetworkMap, erro
|
||||
if peer == nil {
|
||||
return nil, status.Errorf(status.NotFound, "peer with ID %s not found", peerID)
|
||||
}
|
||||
|
||||
aclPeers := account.getPeersByACL(peerID)
|
||||
// Please mind, that the returned route.Route objects will contain Peer.Key instead of Peer.ID.
|
||||
routesUpdate := account.getRoutesToSync(peerID, aclPeers)
|
||||
|
||||
dnsManagementStatus := account.getPeerDNSManagementStatus(peerID)
|
||||
dnsUpdate := nbdns.Config{
|
||||
ServiceEnable: dnsManagementStatus,
|
||||
}
|
||||
|
||||
if dnsManagementStatus {
|
||||
var zones []nbdns.CustomZone
|
||||
peersCustomZone := getPeersCustomZone(account, am.dnsDomain)
|
||||
if peersCustomZone.Domain != "" {
|
||||
zones = append(zones, peersCustomZone)
|
||||
}
|
||||
dnsUpdate.CustomZones = zones
|
||||
dnsUpdate.NameServerGroups = getPeerNSGroups(account, peerID)
|
||||
}
|
||||
|
||||
return &NetworkMap{
|
||||
Peers: aclPeers,
|
||||
Network: account.Network.Copy(),
|
||||
Routes: routesUpdate,
|
||||
DNSConfig: dnsUpdate,
|
||||
}, err
|
||||
return account.GetPeerNetworkMap(peer.ID, am.dnsDomain), nil
|
||||
}
|
||||
|
||||
// GetPeerNetwork returns the Network for a given peer
|
||||
func (am *DefaultAccountManager) GetPeerNetwork(peerID string) (*Network, error) {
|
||||
|
||||
account, err := am.Store.GetAccountByPeerID(peerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -471,13 +441,17 @@ func (am *DefaultAccountManager) GetPeerNetwork(peerID string) (*Network, error)
|
||||
}
|
||||
|
||||
// AddPeer adds a new peer to the Store.
|
||||
// Each Account has a list of pre-authorised SetupKey and if no Account has a given key err with a code codes.Unauthenticated
|
||||
// will be returned, meaning the key is invalid
|
||||
// Each Account has a list of pre-authorized SetupKey and if no Account has a given key err with a code status.PermissionDenied
|
||||
// will be returned, meaning the setup key is invalid or not found.
|
||||
// If a User ID is provided, it means that we passed the authentication using JWT, then we look for account by User ID and register the peer
|
||||
// to it. We also add the User ID to the peer metadata to identify registrant.
|
||||
// to it. We also add the User ID to the peer metadata to identify registrant. If no userID provided, then fail with status.PermissionDenied
|
||||
// Each new Peer will be assigned a new next net.IP from the Account.Network and Account.Network.LastIP will be updated (IP's are not reused).
|
||||
// The peer property is just a placeholder for the Peer properties to pass further
|
||||
func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*Peer, error) {
|
||||
func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*Peer, *NetworkMap, error) {
|
||||
if setupKey == "" && userID == "" {
|
||||
// no auth method provided => reject access
|
||||
return nil, nil, status.Errorf(status.Unauthenticated, "no peer auth method provided, please use a setup key or interactive SSO login")
|
||||
}
|
||||
|
||||
upperKey := strings.ToUpper(setupKey)
|
||||
var account *Account
|
||||
@@ -490,7 +464,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*
|
||||
account, err = am.Store.GetAccountBySetupKey(setupKey)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, status.Errorf(status.NotFound, "failed adding new peer: account not found")
|
||||
return nil, nil, status.Errorf(status.NotFound, "failed adding new peer: account not found")
|
||||
}
|
||||
|
||||
unlock := am.Store.AcquireAccountLock(account.Id)
|
||||
@@ -499,7 +473,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*
|
||||
// ensure that we consider modification happened meanwhile (because we were outside the account lock when we fetched the account)
|
||||
account, err = am.Store.GetAccount(account.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
opEvent := &activity.Event{
|
||||
@@ -511,11 +485,11 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*
|
||||
// validate the setup key if adding with a key
|
||||
sk, err := account.FindSetupKey(upperKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if !sk.IsValid() {
|
||||
return nil, status.Errorf(status.PreconditionFailed, "couldn't add peer: setup key is invalid")
|
||||
return nil, nil, status.Errorf(status.PreconditionFailed, "couldn't add peer: setup key is invalid")
|
||||
}
|
||||
|
||||
account.SetupKeys[sk.Key] = sk.IncrementUsage()
|
||||
@@ -529,16 +503,16 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*
|
||||
takenIps := account.getTakenIPs()
|
||||
existingLabels := account.getPeerDNSLabels()
|
||||
|
||||
newLabel, err := getPeerHostLabel(peer.Name, existingLabels)
|
||||
newLabel, err := getPeerHostLabel(peer.Meta.Hostname, existingLabels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
peer.DNSLabel = newLabel
|
||||
network := account.Network
|
||||
nextIp, err := AllocatePeerIP(network.Net, takenIps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
newPeer := &Peer{
|
||||
@@ -547,7 +521,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*
|
||||
SetupKey: upperKey,
|
||||
IP: nextIp,
|
||||
Meta: peer.Meta,
|
||||
Name: peer.Name,
|
||||
Name: peer.Meta.Hostname,
|
||||
DNSLabel: newLabel,
|
||||
UserID: userID,
|
||||
Status: &PeerStatus{Connected: false, LastSeen: time.Now()},
|
||||
@@ -560,7 +534,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*
|
||||
// add peer to 'All' group
|
||||
group, err := account.GetGroupAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
group.Peers = append(group.Peers, newPeer.ID)
|
||||
|
||||
@@ -568,12 +542,12 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*
|
||||
if addedByUser {
|
||||
groupsToAdd, err = account.getUserGroups(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
} else {
|
||||
groupsToAdd, err = account.getSetupKeyGroups(upperKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,55 +563,184 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*
|
||||
account.Network.IncSerial()
|
||||
err = am.Store.SaveAccount(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
opEvent.TargetID = newPeer.ID
|
||||
opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain())
|
||||
am.storeEvent(opEvent.InitiatorID, opEvent.TargetID, opEvent.AccountID, opEvent.Activity, opEvent.Meta)
|
||||
|
||||
return newPeer, nil
|
||||
}
|
||||
|
||||
// UpdatePeerLastLogin sets Peer.LastLogin to the current timestamp.
|
||||
func (am *DefaultAccountManager) UpdatePeerLastLogin(peerID string) error {
|
||||
account, err := am.Store.GetAccountByPeerID(peerID)
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
networkMap := account.GetPeerNetworkMap(peer.ID, am.dnsDomain)
|
||||
return newPeer, networkMap, nil
|
||||
}
|
||||
|
||||
// SyncPeer checks whether peer is eligible for receiving NetworkMap (authenticated) and returns its NetworkMap if eligible
|
||||
func (am *DefaultAccountManager) SyncPeer(sync PeerSync) (*Peer, *NetworkMap, error) {
|
||||
account, err := am.Store.GetAccountByPeerPubKey(sync.WireGuardPubKey)
|
||||
if err != nil {
|
||||
if errStatus, ok := status.FromError(err); ok && errStatus.Type() == status.NotFound {
|
||||
return nil, nil, status.Errorf(status.Unauthenticated, "peer is not registered")
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// we found the peer, and we follow a normal login flow
|
||||
unlock := am.Store.AcquireAccountLock(account.Id)
|
||||
defer unlock()
|
||||
|
||||
// ensure that we consider modification happened meanwhile (because we were outside the account lock when we fetched the account)
|
||||
// fetch the account from the store once more after acquiring lock to avoid concurrent updates inconsistencies
|
||||
account, err = am.Store.GetAccount(account.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
peer := account.GetPeer(peerID)
|
||||
if peer == nil {
|
||||
return status.Errorf(status.NotFound, "peer with ID %s not found", peerID)
|
||||
peer, err := account.FindPeerByPubKey(sync.WireGuardPubKey)
|
||||
if err != nil {
|
||||
return nil, nil, status.Errorf(status.Unauthenticated, "peer is not registered")
|
||||
}
|
||||
|
||||
peer.LastLogin = time.Now()
|
||||
newStatus := peer.Status.Copy()
|
||||
newStatus.LoginExpired = false
|
||||
peer.Status = newStatus
|
||||
if peerLoginExpired(peer, account) {
|
||||
return nil, nil, status.Errorf(status.PermissionDenied, "peer login has expired, please log in once more")
|
||||
}
|
||||
return peer, account.GetPeerNetworkMap(peer.ID, am.dnsDomain), nil
|
||||
}
|
||||
|
||||
account.UpdatePeer(peer)
|
||||
// LoginPeer logs in or registers a peer.
|
||||
// If peer doesn't exist the function checks whether a setup key or a user is present and registers a new peer if so.
|
||||
func (am *DefaultAccountManager) LoginPeer(login PeerLogin) (*Peer, *NetworkMap, error) {
|
||||
account, err := am.Store.GetAccountByPeerPubKey(login.WireGuardPubKey)
|
||||
if err != nil {
|
||||
if errStatus, ok := status.FromError(err); ok && errStatus.Type() == status.NotFound {
|
||||
// we couldn't find this peer by its public key which can mean that peer hasn't been registered yet.
|
||||
// Try registering it.
|
||||
return am.AddPeer(login.SetupKey, login.UserID, &Peer{
|
||||
Key: login.WireGuardPubKey,
|
||||
Meta: login.Meta,
|
||||
SSHKey: login.SSHKey,
|
||||
})
|
||||
}
|
||||
log.Errorf("failed while logging in peer %s: %v", login.WireGuardPubKey, err)
|
||||
return nil, nil, status.Errorf(status.Internal, "failed while logging in peer")
|
||||
}
|
||||
|
||||
// we found the peer, and we follow a normal login flow
|
||||
unlock := am.Store.AcquireAccountLock(account.Id)
|
||||
defer unlock()
|
||||
|
||||
// fetch the account from the store once more after acquiring lock to avoid concurrent updates inconsistencies
|
||||
account, err = am.Store.GetAccount(account.Id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
peer, err := account.FindPeerByPubKey(login.WireGuardPubKey)
|
||||
if err != nil {
|
||||
return nil, nil, status.Errorf(status.Unauthenticated, "peer is not registered")
|
||||
}
|
||||
|
||||
updateRemotePeers := false
|
||||
if peerLoginExpired(peer, account) {
|
||||
err = checkAuth(login.UserID, peer)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// If peer was expired before and if it reached this point, it is re-authenticated.
|
||||
// UserID is present, meaning that JWT validation passed successfully in the API layer.
|
||||
updatePeerLastLogin(peer, account)
|
||||
updateRemotePeers = true
|
||||
}
|
||||
|
||||
peer = updatePeerMeta(peer, login.Meta, account)
|
||||
|
||||
peer, err = am.checkAndUpdatePeerSSHKey(peer, account, login.SSHKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = am.Store.SaveAccount(account)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
if updateRemotePeers {
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
return peer, account.GetPeerNetworkMap(peer.ID, am.dnsDomain), nil
|
||||
}
|
||||
|
||||
func checkAuth(loginUserID string, peer *Peer) error {
|
||||
if loginUserID == "" {
|
||||
// absence of a user ID indicates that JWT wasn't provided.
|
||||
return status.Errorf(status.PermissionDenied, "peer login has expired, please log in once more")
|
||||
}
|
||||
if peer.UserID != loginUserID {
|
||||
log.Warnf("user mismatch when loggin in peer %s: peer user %s, login user %s ", peer.ID, peer.UserID, loginUserID)
|
||||
return status.Errorf(status.Unauthenticated, "can't login")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func peerLoginExpired(peer *Peer, account *Account) bool {
|
||||
expired, expiresIn := peer.LoginExpired(account.Settings.PeerLoginExpiration)
|
||||
expired = account.Settings.PeerLoginExpirationEnabled && expired
|
||||
if expired || peer.Status.LoginExpired {
|
||||
log.Debugf("peer's %s login expired %v ago", peer.ID, expiresIn)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func updatePeerLastLogin(peer *Peer, account *Account) {
|
||||
peer.UpdateLastLogin()
|
||||
account.UpdatePeer(peer)
|
||||
}
|
||||
|
||||
// UpdateLastLogin and set login expired false
|
||||
func (p *Peer) UpdateLastLogin() *Peer {
|
||||
p.LastLogin = time.Now()
|
||||
newStatus := p.Status.Copy()
|
||||
newStatus.LoginExpired = false
|
||||
p.Status = newStatus
|
||||
return p
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) checkAndUpdatePeerSSHKey(peer *Peer, account *Account, newSSHKey string) (*Peer, error) {
|
||||
if len(newSSHKey) == 0 {
|
||||
log.Debugf("no new SSH key provided for peer %s, skipping update", peer.ID)
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
return nil
|
||||
if peer.SSHKey == newSSHKey {
|
||||
log.Debugf("same SSH key provided for peer %s, skipping update", peer.ID)
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
peer.SSHKey = newSSHKey
|
||||
account.UpdatePeer(peer)
|
||||
|
||||
err := am.Store.SaveAccount(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// trigger network map update
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
// UpdatePeerSSHKey updates peer's public SSH key
|
||||
func (am *DefaultAccountManager) UpdatePeerSSHKey(peerID string, sshKey string) error {
|
||||
|
||||
if sshKey == "" {
|
||||
log.Debugf("empty SSH key provided for peer %s, skipping update", peerID)
|
||||
return nil
|
||||
@@ -681,7 +784,6 @@ func (am *DefaultAccountManager) UpdatePeerSSHKey(peerID string, sshKey string)
|
||||
|
||||
// GetPeer for a given accountID, peerID and userID error if not found.
|
||||
func (am *DefaultAccountManager) GetPeer(accountID, peerID, userID string) (*Peer, error) {
|
||||
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
@@ -713,7 +815,7 @@ func (am *DefaultAccountManager) GetPeer(accountID, peerID, userID string) (*Pee
|
||||
}
|
||||
|
||||
for _, p := range userPeers {
|
||||
aclPeers := account.getPeersByACL(p.ID)
|
||||
aclPeers, _ := account.getPeersByPolicy(p.ID)
|
||||
for _, aclPeer := range aclPeers {
|
||||
if aclPeer.ID == peerID {
|
||||
return peer, nil
|
||||
@@ -724,95 +826,10 @@ func (am *DefaultAccountManager) GetPeer(accountID, peerID, userID string) (*Pee
|
||||
return nil, status.Errorf(status.Internal, "user %s has no access to peer %s under account %s", userID, peerID, accountID)
|
||||
}
|
||||
|
||||
// UpdatePeerMeta updates peer's system metadata
|
||||
func (am *DefaultAccountManager) UpdatePeerMeta(peerID string, meta PeerSystemMeta) error {
|
||||
|
||||
account, err := am.Store.GetAccountByPeerID(peerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unlock := am.Store.AcquireAccountLock(account.Id)
|
||||
defer unlock()
|
||||
|
||||
peer := account.GetPeer(peerID)
|
||||
if peer == nil {
|
||||
return status.Errorf(status.NotFound, "peer with ID %s not found", peerID)
|
||||
}
|
||||
|
||||
// Avoid overwriting UIVersion if the update was triggered sole by the CLI client
|
||||
if meta.UIVersion == "" {
|
||||
meta.UIVersion = peer.Meta.UIVersion
|
||||
}
|
||||
|
||||
peer.Meta = meta
|
||||
func updatePeerMeta(peer *Peer, meta PeerSystemMeta, account *Account) *Peer {
|
||||
peer.UpdateMeta(meta)
|
||||
account.UpdatePeer(peer)
|
||||
|
||||
err = am.Store.SaveAccount(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getPeersByACL returns all peers that given peer has access to.
|
||||
func (a *Account) getPeersByACL(peerID string) []*Peer {
|
||||
var peers []*Peer
|
||||
srcRules, dstRules := a.GetPeerRules(peerID)
|
||||
|
||||
groups := map[string]*Group{}
|
||||
for _, r := range srcRules {
|
||||
if r.Disabled {
|
||||
continue
|
||||
}
|
||||
if r.Flow == TrafficFlowBidirect {
|
||||
for _, gid := range r.Destination {
|
||||
if group, ok := a.Groups[gid]; ok {
|
||||
groups[gid] = group
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range dstRules {
|
||||
if r.Disabled {
|
||||
continue
|
||||
}
|
||||
if r.Flow == TrafficFlowBidirect {
|
||||
for _, gid := range r.Source {
|
||||
if group, ok := a.Groups[gid]; ok {
|
||||
groups[gid] = group
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
peersSet := make(map[string]struct{})
|
||||
for _, g := range groups {
|
||||
for _, pid := range g.Peers {
|
||||
peer, ok := a.Peers[pid]
|
||||
if !ok {
|
||||
log.Warnf(
|
||||
"peer %s found in group %s but doesn't belong to account %s",
|
||||
pid,
|
||||
g.ID,
|
||||
a.Id,
|
||||
)
|
||||
continue
|
||||
}
|
||||
expired, _ := peer.LoginExpired(a.Settings.PeerLoginExpiration)
|
||||
if expired {
|
||||
continue
|
||||
}
|
||||
// exclude original peer
|
||||
if _, ok := peersSet[peer.ID]; peer.ID != peerID && !ok {
|
||||
peersSet[peer.ID] = struct{}{}
|
||||
peers = append(peers, peer.Copy())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return peers
|
||||
return peer
|
||||
}
|
||||
|
||||
// updateAccountPeers updates all peers that belong to an account.
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/rs/xid"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
func TestPeer_LoginExpired(t *testing.T) {
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
expirationEnabled bool
|
||||
@@ -55,6 +55,7 @@ func TestPeer_LoginExpired(t *testing.T) {
|
||||
peer := &Peer{
|
||||
LoginExpirationEnabled: c.expirationEnabled,
|
||||
LastLogin: c.lastLogin,
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
expired, _ := peer.LoginExpired(c.accountSettings.PeerLoginExpiration)
|
||||
@@ -90,12 +91,10 @@ func TestAccountManager_GetNetworkMap(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
peer1, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
peer1, _, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
Key: peerKey1.PublicKey().String(),
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: "test-peer-2",
|
||||
Meta: PeerSystemMeta{Hostname: "test-peer-1"},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("expecting peer to be added, got failure %v", err)
|
||||
return
|
||||
@@ -106,10 +105,9 @@ func TestAccountManager_GetNetworkMap(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
_, err = manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
_, _, err = manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
Key: peerKey2.PublicKey().String(),
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: "test-peer-2",
|
||||
Meta: PeerSystemMeta{Hostname: "test-peer-2"},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -125,6 +123,7 @@ func TestAccountManager_GetNetworkMap(t *testing.T) {
|
||||
|
||||
if len(networkMap.Peers) != 1 {
|
||||
t.Errorf("expecting Account NetworkMap to have 1 peers, got %v", len(networkMap.Peers))
|
||||
return
|
||||
}
|
||||
|
||||
if networkMap.Peers[0].Key != peerKey2.PublicKey().String() {
|
||||
@@ -136,7 +135,7 @@ func TestAccountManager_GetNetworkMap(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountManager_GetNetworkMapWithRule(t *testing.T) {
|
||||
func TestAccountManager_GetNetworkMapWithPolicy(t *testing.T) {
|
||||
manager, err := createManager(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -163,12 +162,10 @@ func TestAccountManager_GetNetworkMapWithRule(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
peer1, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
peer1, _, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
Key: peerKey1.PublicKey().String(),
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: "test-peer-2",
|
||||
Meta: PeerSystemMeta{Hostname: "test-peer-1"},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("expecting peer to be added, got failure %v", err)
|
||||
return
|
||||
@@ -179,24 +176,22 @@ func TestAccountManager_GetNetworkMapWithRule(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
peer2, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
peer2, _, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
Key: peerKey2.PublicKey().String(),
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: "test-peer-2",
|
||||
Meta: PeerSystemMeta{Hostname: "test-peer-2"},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("expecting peer to be added, got failure %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
rules, err := manager.ListRules(account.Id, userID)
|
||||
policies, err := manager.ListPolicies(account.Id, userID)
|
||||
if err != nil {
|
||||
t.Errorf("expecting to get a list of rules, got failure %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = manager.DeleteRule(account.Id, rules[0].ID, userID)
|
||||
err = manager.DeletePolicy(account.Id, policies[0].ID, userID)
|
||||
if err != nil {
|
||||
t.Errorf("expecting to delete 1 group, got failure %v", err)
|
||||
return
|
||||
@@ -204,14 +199,14 @@ func TestAccountManager_GetNetworkMapWithRule(t *testing.T) {
|
||||
var (
|
||||
group1 Group
|
||||
group2 Group
|
||||
rule Rule
|
||||
policy Policy
|
||||
)
|
||||
|
||||
group1.ID = xid.New().String()
|
||||
group2.ID = xid.New().String()
|
||||
group1.Name = "src"
|
||||
group2.Name = "dst"
|
||||
rule.ID = xid.New().String()
|
||||
policy.ID = xid.New().String()
|
||||
group1.Peers = append(group1.Peers, peer1.ID)
|
||||
group2.Peers = append(group2.Peers, peer2.ID)
|
||||
|
||||
@@ -226,11 +221,21 @@ func TestAccountManager_GetNetworkMapWithRule(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
rule.Name = "test"
|
||||
rule.Source = append(rule.Source, group1.ID)
|
||||
rule.Destination = append(rule.Destination, group2.ID)
|
||||
rule.Flow = TrafficFlowBidirect
|
||||
err = manager.SaveRule(account.Id, userID, &rule)
|
||||
policy.Name = "test"
|
||||
policy.Enabled = true
|
||||
policy.Rules = []*PolicyRule{
|
||||
{
|
||||
Enabled: true,
|
||||
Sources: []string{group1.ID},
|
||||
Destinations: []string{group2.ID},
|
||||
Action: PolicyTrafficActionAccept,
|
||||
},
|
||||
}
|
||||
if err := policy.UpdateQueryFromRules(); err != nil {
|
||||
t.Errorf("expecting policy to be updated, got failure %v", err)
|
||||
return
|
||||
}
|
||||
err = manager.SavePolicy(account.Id, userID, &policy)
|
||||
if err != nil {
|
||||
t.Errorf("expecting rule to be added, got failure %v", err)
|
||||
return
|
||||
@@ -277,8 +282,8 @@ func TestAccountManager_GetNetworkMapWithRule(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
rule.Disabled = true
|
||||
err = manager.SaveRule(account.Id, userID, &rule)
|
||||
policy.Enabled = false
|
||||
err = manager.SavePolicy(account.Id, userID, &policy)
|
||||
if err != nil {
|
||||
t.Errorf("expecting rule to be added, got failure %v", err)
|
||||
return
|
||||
@@ -332,12 +337,10 @@ func TestAccountManager_GetPeerNetwork(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
peer1, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
peer1, _, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
Key: peerKey1.PublicKey().String(),
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: "test-peer-2",
|
||||
Meta: PeerSystemMeta{Hostname: "test-peer-1"},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("expecting peer to be added, got failure %v", err)
|
||||
return
|
||||
@@ -348,10 +351,9 @@ func TestAccountManager_GetPeerNetwork(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
_, err = manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
_, _, err = manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
Key: peerKey2.PublicKey().String(),
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: "test-peer-2",
|
||||
Meta: PeerSystemMeta{Hostname: "test-peer-2"},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -368,7 +370,6 @@ func TestAccountManager_GetPeerNetwork(t *testing.T) {
|
||||
if account.Network.Id != network.Id {
|
||||
t.Errorf("expecting Account Networks ID to be equal, got %s expected %s", network.Id, account.Network.Id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDefaultAccountManager_GetPeer(t *testing.T) {
|
||||
@@ -401,12 +402,10 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
peer1, err := manager.AddPeer("", someUser, &Peer{
|
||||
peer1, _, err := manager.AddPeer("", someUser, &Peer{
|
||||
Key: peerKey1.PublicKey().String(),
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: "test-peer-2",
|
||||
Meta: PeerSystemMeta{Hostname: "test-peer-2"},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("expecting peer to be added, got failure %v", err)
|
||||
return
|
||||
@@ -419,12 +418,10 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) {
|
||||
}
|
||||
|
||||
// the second peer added with a setup key
|
||||
peer2, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
peer2, _, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
Key: peerKey2.PublicKey().String(),
|
||||
Meta: PeerSystemMeta{},
|
||||
Name: "test-peer-2",
|
||||
Meta: PeerSystemMeta{Hostname: "test-peer-2"},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
@@ -446,9 +443,9 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) {
|
||||
}
|
||||
assert.NotNil(t, peer)
|
||||
|
||||
// delete the all-to-all rule so that user's peer1 has no access to peer2
|
||||
for _, rule := range account.Rules {
|
||||
err = manager.DeleteRule(accountID, rule.ID, adminUser)
|
||||
// delete the all-to-all policy so that user's peer1 has no access to peer2
|
||||
for _, policy := range account.Policies {
|
||||
err = manager.DeletePolicy(accountID, policy.ID, adminUser)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
@@ -473,11 +470,9 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) {
|
||||
return
|
||||
}
|
||||
assert.NotNil(t, peer)
|
||||
|
||||
}
|
||||
|
||||
func getSetupKey(account *Account, keyType SetupKeyType) *SetupKey {
|
||||
|
||||
var setupKey *SetupKey
|
||||
for _, key := range account.SetupKeys {
|
||||
if key.Type == keyType {
|
||||
|
||||
451
management/server/policy.go
Normal file
451
management/server/policy.go
Normal file
@@ -0,0 +1,451 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// PolicyUpdateOperationType operation type
|
||||
type PolicyUpdateOperationType int
|
||||
|
||||
// PolicyTrafficActionType action type for the firewall
|
||||
type PolicyTrafficActionType string
|
||||
|
||||
const (
|
||||
// PolicyTrafficActionAccept indicates that the traffic is accepted
|
||||
PolicyTrafficActionAccept = PolicyTrafficActionType("accept")
|
||||
// PolicyTrafficActionDrop indicates that the traffic is dropped
|
||||
PolicyTrafficActionDrop = PolicyTrafficActionType("drop")
|
||||
)
|
||||
|
||||
// PolicyUpdateOperation operation object with type and values to be applied
|
||||
type PolicyUpdateOperation struct {
|
||||
Type PolicyUpdateOperationType
|
||||
Values []string
|
||||
}
|
||||
|
||||
//go:embed rego/default_policy_module.rego
|
||||
var defaultPolicyModule string
|
||||
|
||||
//go:embed rego/default_policy.rego
|
||||
var defaultPolicyText string
|
||||
|
||||
// defaultPolicyTemplate is a template for the default policy
|
||||
var defaultPolicyTemplate = template.Must(template.New("policy").Parse(defaultPolicyText))
|
||||
|
||||
// PolicyRule is the metadata of the policy
|
||||
type PolicyRule struct {
|
||||
// ID of the policy rule
|
||||
ID string
|
||||
|
||||
// Name of the rule visible in the UI
|
||||
Name string
|
||||
|
||||
// Description of the rule visible in the UI
|
||||
Description string
|
||||
|
||||
// Enabled status of rule in the system
|
||||
Enabled bool
|
||||
|
||||
// Action policy accept or drops packets
|
||||
Action PolicyTrafficActionType
|
||||
|
||||
// Destinations policy destination groups
|
||||
Destinations []string
|
||||
|
||||
// Sources policy source groups
|
||||
Sources []string
|
||||
}
|
||||
|
||||
// Copy returns a copy of a policy rule
|
||||
func (pm *PolicyRule) Copy() *PolicyRule {
|
||||
return &PolicyRule{
|
||||
ID: pm.ID,
|
||||
Name: pm.Name,
|
||||
Description: pm.Description,
|
||||
Enabled: pm.Enabled,
|
||||
Action: pm.Action,
|
||||
Destinations: pm.Destinations[:],
|
||||
Sources: pm.Sources[:],
|
||||
}
|
||||
}
|
||||
|
||||
// ToRule converts the PolicyRule to a legacy representation of the Rule (for backwards compatibility)
|
||||
func (pm *PolicyRule) ToRule() *Rule {
|
||||
return &Rule{
|
||||
ID: pm.ID,
|
||||
Name: pm.Name,
|
||||
Description: pm.Description,
|
||||
Disabled: !pm.Enabled,
|
||||
Flow: TrafficFlowBidirect,
|
||||
Destination: pm.Destinations,
|
||||
Source: pm.Sources,
|
||||
}
|
||||
}
|
||||
|
||||
// Policy of the Rego query
|
||||
type Policy struct {
|
||||
// ID of the policy
|
||||
ID string
|
||||
|
||||
// Name of the Policy
|
||||
Name string
|
||||
|
||||
// Description of the policy visible in the UI
|
||||
Description string
|
||||
|
||||
// Enabled status of the policy
|
||||
Enabled bool
|
||||
|
||||
// Query of Rego the policy
|
||||
Query string
|
||||
|
||||
// Rules of the policy
|
||||
Rules []*PolicyRule
|
||||
}
|
||||
|
||||
// Copy returns a copy of the policy.
|
||||
func (p *Policy) Copy() *Policy {
|
||||
c := &Policy{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Description: p.Description,
|
||||
Enabled: p.Enabled,
|
||||
Query: p.Query,
|
||||
}
|
||||
for _, r := range p.Rules {
|
||||
c.Rules = append(c.Rules, r.Copy())
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// EventMeta returns activity event meta related to this policy
|
||||
func (p *Policy) EventMeta() map[string]any {
|
||||
return map[string]any{"name": p.Name}
|
||||
}
|
||||
|
||||
// UpdateQueryFromRules marshals policy rules to Rego string and set it to Query
|
||||
func (p *Policy) UpdateQueryFromRules() error {
|
||||
type templateVars struct {
|
||||
All []string
|
||||
Source []string
|
||||
Destination []string
|
||||
}
|
||||
queries := []string{}
|
||||
for _, r := range p.Rules {
|
||||
if !r.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
buff := new(bytes.Buffer)
|
||||
input := templateVars{
|
||||
All: append(r.Destinations[:], r.Sources...),
|
||||
Source: r.Sources,
|
||||
Destination: r.Destinations,
|
||||
}
|
||||
if err := defaultPolicyTemplate.Execute(buff, input); err != nil {
|
||||
return status.Errorf(status.BadRequest, "failed to update policy query: %v", err)
|
||||
}
|
||||
queries = append(queries, buff.String())
|
||||
}
|
||||
p.Query = strings.Join(queries, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// FirewallRule is a rule of the firewall.
|
||||
type FirewallRule struct {
|
||||
// PeerID of the peer
|
||||
PeerID string
|
||||
|
||||
// PeerIP of the peer
|
||||
PeerIP string
|
||||
|
||||
// Direction of the traffic
|
||||
Direction string
|
||||
|
||||
// Action of the traffic
|
||||
Action string
|
||||
|
||||
// Port of the traffic
|
||||
Port string
|
||||
}
|
||||
|
||||
// parseFromRegoResult parses the Rego result to a FirewallRule.
|
||||
func (f *FirewallRule) parseFromRegoResult(value interface{}) error {
|
||||
object, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid Rego query eval result")
|
||||
}
|
||||
|
||||
peerID, ok := object["ID"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid Rego query eval result peer ID type")
|
||||
}
|
||||
|
||||
peerIP, ok := object["IP"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid Rego query eval result peer IP type")
|
||||
}
|
||||
|
||||
direction, ok := object["Direction"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid Rego query eval result peer direction type")
|
||||
}
|
||||
|
||||
action, ok := object["Action"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid Rego query eval result peer action type")
|
||||
}
|
||||
|
||||
port, ok := object["Port"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid Rego query eval result peer port type")
|
||||
}
|
||||
|
||||
f.PeerID = peerID
|
||||
f.PeerIP = peerIP
|
||||
f.Direction = direction
|
||||
f.Action = action
|
||||
f.Port = port
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRegoQuery returns a initialized Rego object with default rule.
|
||||
func (a *Account) getRegoQuery() (rego.PreparedEvalQuery, error) {
|
||||
queries := []func(*rego.Rego){
|
||||
rego.Query("data.netbird.all"),
|
||||
rego.Module("netbird", defaultPolicyModule),
|
||||
}
|
||||
for i, p := range a.Policies {
|
||||
if !p.Enabled {
|
||||
continue
|
||||
}
|
||||
queries = append(queries, rego.Module(fmt.Sprintf("netbird-%d", i), p.Query))
|
||||
}
|
||||
return rego.New(queries...).PrepareForEval(context.TODO())
|
||||
}
|
||||
|
||||
// getPeersByPolicy returns all peers that given peer has access to.
|
||||
func (a *Account) getPeersByPolicy(peerID string) ([]*Peer, []*FirewallRule) {
|
||||
input := map[string]interface{}{
|
||||
"peer_id": peerID,
|
||||
"peers": a.Peers,
|
||||
"groups": a.Groups,
|
||||
}
|
||||
|
||||
query, err := a.getRegoQuery()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("get Rego query")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
evalResult, err := query.Eval(
|
||||
context.TODO(),
|
||||
rego.EvalInput(input),
|
||||
)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("eval Rego query")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if len(evalResult) == 0 || len(evalResult[0].Expressions) == 0 {
|
||||
log.Trace("empty Rego query eval result")
|
||||
return nil, nil
|
||||
}
|
||||
expressions, ok := evalResult[0].Expressions[0].Value.([]interface{})
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
dst := make(map[string]struct{})
|
||||
src := make(map[string]struct{})
|
||||
peers := make([]*Peer, 0, len(expressions))
|
||||
rules := make([]*FirewallRule, 0, len(expressions))
|
||||
for _, v := range expressions {
|
||||
rule := &FirewallRule{}
|
||||
if err := rule.parseFromRegoResult(v); err != nil {
|
||||
log.WithError(err).Error("parse Rego query eval result")
|
||||
continue
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
switch rule.Direction {
|
||||
case "dst":
|
||||
if _, ok := dst[rule.PeerID]; ok {
|
||||
continue
|
||||
}
|
||||
dst[rule.PeerID] = struct{}{}
|
||||
case "src":
|
||||
if _, ok := src[rule.PeerID]; ok {
|
||||
continue
|
||||
}
|
||||
src[rule.PeerID] = struct{}{}
|
||||
default:
|
||||
log.WithField("direction", rule.Direction).Error("invalid direction")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
added := make(map[string]struct{})
|
||||
if _, ok := src[peerID]; ok {
|
||||
for id := range dst {
|
||||
if _, ok := added[id]; !ok && id != peerID {
|
||||
added[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, ok := dst[peerID]; ok {
|
||||
for id := range src {
|
||||
if _, ok := added[id]; !ok && id != peerID {
|
||||
added[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id := range added {
|
||||
peers = append(peers, a.Peers[id])
|
||||
}
|
||||
return peers, rules
|
||||
}
|
||||
|
||||
// GetPolicy from the store
|
||||
func (am *DefaultAccountManager) GetPolicy(accountID, policyID, userID string) (*Policy, error) {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
account, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := account.FindUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !user.IsAdmin() {
|
||||
return nil, status.Errorf(status.PermissionDenied, "only admins are allowed to view policies")
|
||||
}
|
||||
|
||||
for _, policy := range account.Policies {
|
||||
if policy.ID == policyID {
|
||||
return policy, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, status.Errorf(status.NotFound, "policy with ID %s not found", policyID)
|
||||
}
|
||||
|
||||
// SavePolicy in the store
|
||||
func (am *DefaultAccountManager) SavePolicy(accountID, userID string, policy *Policy) error {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
account, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exists := am.savePolicy(account, policy)
|
||||
|
||||
account.Network.IncSerial()
|
||||
if err = am.Store.SaveAccount(account); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
action := activity.PolicyAdded
|
||||
if exists {
|
||||
action = activity.PolicyUpdated
|
||||
}
|
||||
am.storeEvent(userID, policy.ID, accountID, action, policy.EventMeta())
|
||||
|
||||
return am.updateAccountPeers(account)
|
||||
}
|
||||
|
||||
// DeletePolicy from the store
|
||||
func (am *DefaultAccountManager) DeletePolicy(accountID, policyID, userID string) error {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
account, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
policy, err := am.deletePolicy(account, policyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account.Network.IncSerial()
|
||||
if err = am.Store.SaveAccount(account); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
am.storeEvent(userID, policy.ID, accountID, activity.PolicyRemoved, policy.EventMeta())
|
||||
|
||||
return am.updateAccountPeers(account)
|
||||
}
|
||||
|
||||
// ListPolicies from the store
|
||||
func (am *DefaultAccountManager) ListPolicies(accountID, userID string) ([]*Policy, error) {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
account, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := account.FindUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !user.IsAdmin() {
|
||||
return nil, status.Errorf(status.PermissionDenied, "Only Administrators can view policies")
|
||||
}
|
||||
|
||||
return account.Policies[:], nil
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) deletePolicy(account *Account, policyID string) (*Policy, error) {
|
||||
policyIdx := -1
|
||||
for i, policy := range account.Policies {
|
||||
if policy.ID == policyID {
|
||||
policyIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if policyIdx < 0 {
|
||||
return nil, status.Errorf(status.NotFound, "rule with ID %s doesn't exist", policyID)
|
||||
}
|
||||
|
||||
policy := account.Policies[policyIdx]
|
||||
account.Policies = append(account.Policies[:policyIdx], account.Policies[policyIdx+1:]...)
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) savePolicy(account *Account, policy *Policy) (exists bool) {
|
||||
for i, p := range account.Policies {
|
||||
if p.ID == policy.ID {
|
||||
account.Policies[i] = policy
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
account.Policies = append(account.Policies, policy)
|
||||
}
|
||||
return
|
||||
}
|
||||
67
management/server/policy_test.go
Normal file
67
management/server/policy_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAccount_getPeersByPolicy(t *testing.T) {
|
||||
account := &Account{
|
||||
Peers: map[string]*Peer{
|
||||
"peer1": {
|
||||
ID: "peer1",
|
||||
IP: net.IPv4(10, 20, 0, 1),
|
||||
},
|
||||
"peer2": {
|
||||
ID: "peer2",
|
||||
IP: net.IPv4(10, 20, 0, 2),
|
||||
},
|
||||
"peer3": {
|
||||
ID: "peer3",
|
||||
IP: net.IPv4(10, 20, 0, 3),
|
||||
},
|
||||
},
|
||||
Groups: map[string]*Group{
|
||||
"gid1": {
|
||||
ID: "gid1",
|
||||
Name: "all",
|
||||
Peers: []string{"peer1", "peer2", "peer3"},
|
||||
},
|
||||
},
|
||||
Rules: map[string]*Rule{
|
||||
"default": {
|
||||
ID: "default",
|
||||
Name: "default",
|
||||
Description: "default",
|
||||
Disabled: false,
|
||||
Source: []string{"gid1"},
|
||||
Destination: []string{"gid1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rule, err := RuleToPolicy(account.Rules["default"])
|
||||
assert.NoError(t, err)
|
||||
|
||||
account.Policies = append(account.Policies, rule)
|
||||
|
||||
peers, firewallRules := account.getPeersByPolicy("peer1")
|
||||
assert.Len(t, peers, 2)
|
||||
assert.Contains(t, peers, account.Peers["peer2"])
|
||||
assert.Contains(t, peers, account.Peers["peer3"])
|
||||
|
||||
epectedFirewallRules := []*FirewallRule{
|
||||
{PeerID: "peer1", PeerIP: "10.20.0.1", Direction: "dst", Action: "accept", Port: ""},
|
||||
{PeerID: "peer2", PeerIP: "10.20.0.2", Direction: "dst", Action: "accept", Port: ""},
|
||||
{PeerID: "peer3", PeerIP: "10.20.0.3", Direction: "dst", Action: "accept", Port: ""},
|
||||
{PeerID: "peer1", PeerIP: "10.20.0.1", Direction: "src", Action: "accept", Port: ""},
|
||||
{PeerID: "peer2", PeerIP: "10.20.0.2", Direction: "src", Action: "accept", Port: ""},
|
||||
{PeerID: "peer3", PeerIP: "10.20.0.3", Direction: "src", Action: "accept", Port: ""},
|
||||
}
|
||||
assert.Len(t, firewallRules, len(epectedFirewallRules))
|
||||
for i := range firewallRules {
|
||||
assert.Equal(t, firewallRules[i], epectedFirewallRules[i])
|
||||
}
|
||||
}
|
||||
9
management/server/rego/default_policy.rego
Normal file
9
management/server/rego/default_policy.rego
Normal file
@@ -0,0 +1,9 @@
|
||||
package netbird
|
||||
|
||||
all[rule] {
|
||||
is_peer_in_any_group([{{range $i, $e := .All}}{{if $i}},{{end}}"{{$e}}"{{end}}])
|
||||
rule := array.concat(
|
||||
rules_from_groups([{{range $i, $e := .Destination}}{{if $i}},{{end}}"{{$e}}"{{end}}], "dst", "accept", ""),
|
||||
rules_from_groups([{{range $i, $e := .Source}}{{if $i}},{{end}}"{{$e}}"{{end}}], "src", "accept", ""),
|
||||
)[_]
|
||||
}
|
||||
40
management/server/rego/default_policy_module.rego
Normal file
40
management/server/rego/default_policy_module.rego
Normal file
@@ -0,0 +1,40 @@
|
||||
package netbird
|
||||
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
import future.keywords.contains
|
||||
|
||||
# get_rule builds a netbird rule object from given parameters
|
||||
get_rule(peer_id, direction, action, port) := rule if {
|
||||
peer := input.peers[_]
|
||||
peer.ID == peer_id
|
||||
rule := {
|
||||
"ID": peer.ID,
|
||||
"IP": peer.IP,
|
||||
"Direction": direction,
|
||||
"Action": action,
|
||||
"Port": port,
|
||||
}
|
||||
}
|
||||
|
||||
# peers_from_group returns a list of peer ids for a given group id
|
||||
peers_from_group(group_id) := peers if {
|
||||
group := input.groups[_]
|
||||
group.ID == group_id
|
||||
peers := [peer | peer := group.Peers[_]]
|
||||
}
|
||||
|
||||
# netbird_rules_from_groups returns a list of netbird rules for a given list of group names
|
||||
rules_from_groups(groups, direction, action, port) := rules if {
|
||||
group_id := groups[_]
|
||||
rules := [get_rule(peer, direction, action, port) | peer := peers_from_group(group_id)[_]]
|
||||
}
|
||||
|
||||
# is_peer_in_any_group checks that input peer present at least in one group
|
||||
is_peer_in_any_group(groups) := count([group_id]) > 0 if {
|
||||
group_id := groups[_]
|
||||
group := input.groups[_]
|
||||
group.ID == group_id
|
||||
peer := group.Peers[_]
|
||||
peer == input.peer_id
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/netip"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,7 +22,6 @@ const (
|
||||
)
|
||||
|
||||
func TestCreateRoute(t *testing.T) {
|
||||
|
||||
type input struct {
|
||||
network string
|
||||
netID string
|
||||
@@ -265,13 +265,11 @@ func TestCreateRoute(t *testing.T) {
|
||||
if !testCase.expectedRoute.IsEqual(outRoute) {
|
||||
t.Errorf("new route didn't match expected route:\nGot %#v\nExpected:%#v\n", outRoute, testCase.expectedRoute)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveRoute(t *testing.T) {
|
||||
|
||||
validPeer := peer2ID
|
||||
invalidPeer := "nonExisting"
|
||||
validPrefix := netip.MustParsePrefix("192.168.0.0/24")
|
||||
@@ -521,7 +519,6 @@ func TestSaveRoute(t *testing.T) {
|
||||
if !testCase.expectedRoute.IsEqual(savedRoute) {
|
||||
t.Errorf("new route didn't match expected route:\nGot %#v\nExpected:%#v\n", savedRoute, testCase.expectedRoute)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -781,13 +778,11 @@ func TestUpdateRoute(t *testing.T) {
|
||||
if !testCase.expectedRoute.IsEqual(updatedRoute) {
|
||||
t.Errorf("new route didn't match expected route:\nGot %#v\nExpected:%#v\n", updatedRoute, testCase.expectedRoute)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRoute(t *testing.T) {
|
||||
|
||||
testingRoute := &route.Route{
|
||||
ID: "testingRoute",
|
||||
Network: netip.MustParsePrefix("192.168.0.0/16"),
|
||||
@@ -906,20 +901,22 @@ func TestGetNetworkMap_RouteSync(t *testing.T) {
|
||||
err = am.SaveGroup(account.Id, userID, newGroup)
|
||||
require.NoError(t, err)
|
||||
|
||||
rules, err := am.ListRules(account.Id, "testingUser")
|
||||
rules, err := am.ListPolicies(account.Id, "testingUser")
|
||||
require.NoError(t, err)
|
||||
|
||||
defaultRule := rules[0]
|
||||
newRule := defaultRule.Copy()
|
||||
newRule.ID = xid.New().String()
|
||||
newRule.Name = "peer1 only"
|
||||
newRule.Source = []string{newGroup.ID}
|
||||
newRule.Destination = []string{newGroup.ID}
|
||||
|
||||
err = am.SaveRule(account.Id, userID, newRule)
|
||||
newPolicy := defaultRule.Copy()
|
||||
newPolicy.ID = xid.New().String()
|
||||
newPolicy.Name = "peer1 only"
|
||||
newPolicy.Rules[0].Sources = []string{newGroup.ID}
|
||||
newPolicy.Rules[0].Destinations = []string{newGroup.ID}
|
||||
err = newPolicy.UpdateQueryFromRules()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = am.DeleteRule(account.Id, defaultRule.ID, userID)
|
||||
err = am.SavePolicy(account.Id, userID, newPolicy)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = am.DeletePolicy(account.Id, defaultRule.ID, userID)
|
||||
require.NoError(t, err)
|
||||
|
||||
peer1GroupRoutes, err := am.GetNetworkMap(peer1ID)
|
||||
@@ -936,7 +933,6 @@ func TestGetNetworkMap_RouteSync(t *testing.T) {
|
||||
peer1DeletedRoute, err := am.GetNetworkMap(peer1ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, peer1DeletedRoute.Routes, 0, "we should receive one route for peer1")
|
||||
|
||||
}
|
||||
|
||||
func createRouterManager(t *testing.T) (*DefaultAccountManager, error) {
|
||||
@@ -959,7 +955,6 @@ func createRouterStore(t *testing.T) (Store, error) {
|
||||
}
|
||||
|
||||
func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, error) {
|
||||
|
||||
accountID := "testingAcc"
|
||||
domain := "example.com"
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
"strings"
|
||||
)
|
||||
import "fmt"
|
||||
|
||||
// TrafficFlowType defines allowed direction of the traffic in the rule
|
||||
type TrafficFlowType int
|
||||
@@ -18,6 +14,10 @@ const (
|
||||
DefaultRuleName = "Default"
|
||||
// DefaultRuleDescription is a description for the Default rule that is created for every account
|
||||
DefaultRuleDescription = "This is a default rule that allows connections between all the resources"
|
||||
// DefaultPolicyName is a name for the Default policy that is created for every account
|
||||
DefaultPolicyName = "Default"
|
||||
// DefaultPolicyDescription is a description for the Default policy that is created for every account
|
||||
DefaultPolicyDescription = "This is a default policy that allows connections between all the resources"
|
||||
)
|
||||
|
||||
// Rule of ACL for groups
|
||||
@@ -44,38 +44,6 @@ type Rule struct {
|
||||
Flow TrafficFlowType
|
||||
}
|
||||
|
||||
const (
|
||||
// UpdateRuleName indicates a rule name update operation
|
||||
UpdateRuleName RuleUpdateOperationType = iota
|
||||
// UpdateRuleDescription indicates a rule description update operation
|
||||
UpdateRuleDescription
|
||||
// UpdateRuleStatus indicates a rule status update operation
|
||||
UpdateRuleStatus
|
||||
// UpdateRuleFlow indicates a rule flow update operation
|
||||
UpdateRuleFlow
|
||||
// InsertGroupsToSource indicates an insert groups to source rule operation
|
||||
InsertGroupsToSource
|
||||
// RemoveGroupsFromSource indicates an remove groups from source rule operation
|
||||
RemoveGroupsFromSource
|
||||
// UpdateSourceGroups indicates a replacement of source group list of a rule operation
|
||||
UpdateSourceGroups
|
||||
// InsertGroupsToDestination indicates an insert groups to destination rule operation
|
||||
InsertGroupsToDestination
|
||||
// RemoveGroupsFromDestination indicates an remove groups from destination rule operation
|
||||
RemoveGroupsFromDestination
|
||||
// UpdateDestinationGroups indicates a replacement of destination group list of a rule operation
|
||||
UpdateDestinationGroups
|
||||
)
|
||||
|
||||
// RuleUpdateOperationType operation type
|
||||
type RuleUpdateOperationType int
|
||||
|
||||
// RuleUpdateOperation operation object with type and values to be applied
|
||||
type RuleUpdateOperation struct {
|
||||
Type RuleUpdateOperationType
|
||||
Values []string
|
||||
}
|
||||
|
||||
func (r *Rule) Copy() *Rule {
|
||||
return &Rule{
|
||||
ID: r.ID,
|
||||
@@ -93,185 +61,36 @@ func (r *Rule) EventMeta() map[string]any {
|
||||
return map[string]any{"name": r.Name}
|
||||
}
|
||||
|
||||
// GetRule of ACL from the store
|
||||
func (am *DefaultAccountManager) GetRule(accountID, ruleID, userID string) (*Rule, error) {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
account, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// ToPolicyRule converts a Rule to a PolicyRule object
|
||||
func (r *Rule) ToPolicyRule() *PolicyRule {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
user, err := account.FindUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return &PolicyRule{
|
||||
ID: r.ID,
|
||||
Name: r.Name,
|
||||
Enabled: !r.Disabled,
|
||||
Description: r.Description,
|
||||
Action: PolicyTrafficActionAccept,
|
||||
Destinations: r.Destination,
|
||||
Sources: r.Source,
|
||||
}
|
||||
|
||||
if !user.IsAdmin() {
|
||||
return nil, status.Errorf(status.PermissionDenied, "only admins are allowed to view rules")
|
||||
}
|
||||
|
||||
rule, ok := account.Rules[ruleID]
|
||||
if ok {
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
return nil, status.Errorf(status.NotFound, "rule with ID %s not found", ruleID)
|
||||
}
|
||||
|
||||
// SaveRule of ACL in the store
|
||||
func (am *DefaultAccountManager) SaveRule(accountID, userID string, rule *Rule) error {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
account, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, exists := account.Rules[rule.ID]
|
||||
|
||||
account.Rules[rule.ID] = rule
|
||||
|
||||
account.Network.IncSerial()
|
||||
if err = am.Store.SaveAccount(account); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
action := activity.RuleAdded
|
||||
if exists {
|
||||
action = activity.RuleUpdated
|
||||
}
|
||||
am.storeEvent(userID, rule.ID, accountID, action, rule.EventMeta())
|
||||
|
||||
return am.updateAccountPeers(account)
|
||||
}
|
||||
|
||||
// UpdateRule updates a rule using a list of operations
|
||||
func (am *DefaultAccountManager) UpdateRule(accountID string, ruleID string,
|
||||
operations []RuleUpdateOperation) (*Rule, error) {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
account, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ruleToUpdate, ok := account.Rules[ruleID]
|
||||
if !ok {
|
||||
return nil, status.Errorf(status.NotFound, "rule %s no longer exists", ruleID)
|
||||
}
|
||||
|
||||
rule := ruleToUpdate.Copy()
|
||||
|
||||
for _, operation := range operations {
|
||||
switch operation.Type {
|
||||
case UpdateRuleName:
|
||||
rule.Name = operation.Values[0]
|
||||
case UpdateRuleDescription:
|
||||
rule.Description = operation.Values[0]
|
||||
case UpdateRuleFlow:
|
||||
if operation.Values[0] != TrafficFlowBidirectString {
|
||||
return nil, status.Errorf(status.InvalidArgument, "failed to parse flow")
|
||||
}
|
||||
rule.Flow = TrafficFlowBidirect
|
||||
case UpdateRuleStatus:
|
||||
if strings.ToLower(operation.Values[0]) == "true" {
|
||||
rule.Disabled = true
|
||||
} else if strings.ToLower(operation.Values[0]) == "false" {
|
||||
rule.Disabled = false
|
||||
} else {
|
||||
return nil, status.Errorf(status.InvalidArgument, "failed to parse status")
|
||||
}
|
||||
case UpdateSourceGroups:
|
||||
rule.Source = operation.Values
|
||||
case InsertGroupsToSource:
|
||||
sourceList := rule.Source
|
||||
resultList := removeFromList(sourceList, operation.Values)
|
||||
rule.Source = append(resultList, operation.Values...)
|
||||
case RemoveGroupsFromSource:
|
||||
sourceList := rule.Source
|
||||
resultList := removeFromList(sourceList, operation.Values)
|
||||
rule.Source = resultList
|
||||
case UpdateDestinationGroups:
|
||||
rule.Destination = operation.Values
|
||||
case InsertGroupsToDestination:
|
||||
sourceList := rule.Destination
|
||||
resultList := removeFromList(sourceList, operation.Values)
|
||||
rule.Destination = append(resultList, operation.Values...)
|
||||
case RemoveGroupsFromDestination:
|
||||
sourceList := rule.Destination
|
||||
resultList := removeFromList(sourceList, operation.Values)
|
||||
rule.Destination = resultList
|
||||
}
|
||||
}
|
||||
|
||||
account.Rules[ruleID] = rule
|
||||
|
||||
account.Network.IncSerial()
|
||||
if err = am.Store.SaveAccount(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = am.updateAccountPeers(account)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(status.Internal, "failed to update account peers")
|
||||
}
|
||||
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
// DeleteRule of ACL from the store
|
||||
func (am *DefaultAccountManager) DeleteRule(accountID, ruleID, userID string) error {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
account, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rule := account.Rules[ruleID]
|
||||
// RuleToPolicy converts a Rule to a Policy query object
|
||||
func RuleToPolicy(rule *Rule) (*Policy, error) {
|
||||
if rule == nil {
|
||||
return status.Errorf(status.NotFound, "rule with ID %s doesn't exist", ruleID)
|
||||
return nil, fmt.Errorf("rule is empty")
|
||||
}
|
||||
delete(account.Rules, ruleID)
|
||||
|
||||
account.Network.IncSerial()
|
||||
if err = am.Store.SaveAccount(account); err != nil {
|
||||
return err
|
||||
policy := &Policy{
|
||||
ID: rule.ID,
|
||||
Name: rule.Name,
|
||||
Description: rule.Description,
|
||||
Enabled: !rule.Disabled,
|
||||
Rules: []*PolicyRule{rule.ToPolicyRule()},
|
||||
}
|
||||
|
||||
am.storeEvent(userID, rule.ID, accountID, activity.RuleRemoved, rule.EventMeta())
|
||||
|
||||
return am.updateAccountPeers(account)
|
||||
}
|
||||
|
||||
// ListRules of ACL from the store
|
||||
func (am *DefaultAccountManager) ListRules(accountID, userID string) ([]*Rule, error) {
|
||||
unlock := am.Store.AcquireAccountLock(accountID)
|
||||
defer unlock()
|
||||
|
||||
account, err := am.Store.GetAccount(accountID)
|
||||
if err != nil {
|
||||
if err := policy.UpdateQueryFromRules(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := account.FindUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !user.IsAdmin() {
|
||||
return nil, status.Errorf(status.PermissionDenied, "Only Administrators can view Access Rules")
|
||||
}
|
||||
|
||||
rules := make([]*Rule, 0, len(account.Rules))
|
||||
for _, item := range account.Rules {
|
||||
rules = append(rules, item)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ const (
|
||||
|
||||
// BadRequest indicates that user is not authorized
|
||||
BadRequest Type = 9
|
||||
|
||||
// Unauthenticated indicates that user is not authenticated due to absence of valid credentials
|
||||
Unauthenticated Type = 10
|
||||
)
|
||||
|
||||
// Type is a type of the Error
|
||||
|
||||
116
management/server/testdata/store_policy_migrate.json
vendored
Normal file
116
management/server/testdata/store_policy_migrate.json
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"Accounts": {
|
||||
"bf1c8084-ba50-4ce7-9439-34653001fc3b": {
|
||||
"Id": "bf1c8084-ba50-4ce7-9439-34653001fc3b",
|
||||
"Domain": "test.com",
|
||||
"DomainCategory": "private",
|
||||
"IsDomainPrimaryAccount": true,
|
||||
"SetupKeys": {
|
||||
"A2C8E62B-38F5-4553-B31E-DD66C696CEBB": {
|
||||
"Key": "A2C8E62B-38F5-4553-B31E-DD66C696CEBB",
|
||||
"Name": "Default key",
|
||||
"Type": "reusable",
|
||||
"CreatedAt": "2021-08-19T20:46:20.005936822+02:00",
|
||||
"ExpiresAt": "2321-09-18T20:46:20.005936822+02:00",
|
||||
"Revoked": false,
|
||||
"UsedTimes": 0
|
||||
}
|
||||
},
|
||||
"Network": {
|
||||
"Id": "af1c8024-ha40-4ce2-9418-34653101fc3c",
|
||||
"Net": {
|
||||
"IP": "100.64.0.0",
|
||||
"Mask": "//8AAA=="
|
||||
},
|
||||
"Dns": null
|
||||
},
|
||||
"Peers": {
|
||||
"cfefqs706sqkneg59g4g": {
|
||||
"ID": "cfefqs706sqkneg59g4g",
|
||||
"Key": "MI5mHfJhbggPfD3FqEIsXm8X5bSWeUI2LhO9MpEEtWA=",
|
||||
"SetupKey": "",
|
||||
"IP": "100.103.179.238",
|
||||
"Meta": {
|
||||
"Hostname": "Ubuntu-2204-jammy-amd64-base",
|
||||
"GoOS": "linux",
|
||||
"Kernel": "Linux",
|
||||
"Core": "22.04",
|
||||
"Platform": "x86_64",
|
||||
"OS": "Ubuntu",
|
||||
"WtVersion": "development",
|
||||
"UIVersion": ""
|
||||
},
|
||||
"Name": "crocodile",
|
||||
"DNSLabel": "crocodile",
|
||||
"Status": {
|
||||
"LastSeen": "2023-02-13T12:37:12.635454796Z",
|
||||
"Connected": true
|
||||
},
|
||||
"UserID": "edafee4e-63fb-11ec-90d6-0242ac120003",
|
||||
"SSHKey": "AAAAC3NzaC1lZDI1NTE5AAAAIJN1NM4bpB9K",
|
||||
"SSHEnabled": false
|
||||
},
|
||||
"cfeg6sf06sqkneg59g50": {
|
||||
"ID": "cfeg6sf06sqkneg59g50",
|
||||
"Key": "zMAOKUeIYIuun4n0xPR1b3IdYZPmsyjYmB2jWCuloC4=",
|
||||
"SetupKey": "",
|
||||
"IP": "100.103.26.180",
|
||||
"Meta": {
|
||||
"Hostname": "borg",
|
||||
"GoOS": "linux",
|
||||
"Kernel": "Linux",
|
||||
"Core": "22.04",
|
||||
"Platform": "x86_64",
|
||||
"OS": "Ubuntu",
|
||||
"WtVersion": "development",
|
||||
"UIVersion": ""
|
||||
},
|
||||
"Name": "dingo",
|
||||
"DNSLabel": "dingo",
|
||||
"Status": {
|
||||
"LastSeen": "2023-02-21T09:37:42.565899199Z",
|
||||
"Connected": false
|
||||
},
|
||||
"UserID": "f4f6d672-63fb-11ec-90d6-0242ac120003",
|
||||
"SSHKey": "AAAAC3NzaC1lZDI1NTE5AAAAILHW",
|
||||
"SSHEnabled": true
|
||||
}
|
||||
},
|
||||
"Groups": {
|
||||
"cfefqs706sqkneg59g3g": {
|
||||
"ID": "cfefqs706sqkneg59g3g",
|
||||
"Name": "All",
|
||||
"Peers": [
|
||||
"cfefqs706sqkneg59g4g",
|
||||
"cfeg6sf06sqkneg59g50"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Rules": {
|
||||
"cfefqs706sqkneg59g40": {
|
||||
"ID": "cfefqs706sqkneg59g40",
|
||||
"Name": "Default",
|
||||
"Description": "This is a default rule that allows connections between all the resources",
|
||||
"Disabled": false,
|
||||
"Source": [
|
||||
"cfefqs706sqkneg59g3g"
|
||||
],
|
||||
"Destination": [
|
||||
"cfefqs706sqkneg59g3g"
|
||||
],
|
||||
"Flow": 0
|
||||
}
|
||||
},
|
||||
"Users": {
|
||||
"edafee4e-63fb-11ec-90d6-0242ac120003": {
|
||||
"Id": "edafee4e-63fb-11ec-90d6-0242ac120003",
|
||||
"Role": "admin"
|
||||
},
|
||||
"f4f6d672-63fb-11ec-90d6-0242ac120003": {
|
||||
"Id": "f4f6d672-63fb-11ec-90d6-0242ac120003",
|
||||
"Role": "user"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
130
management/server/testdata/store_with_expired_peers.json
vendored
Normal file
130
management/server/testdata/store_with_expired_peers.json
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"Accounts": {
|
||||
"bf1c8084-ba50-4ce7-9439-34653001fc3b": {
|
||||
"Id": "bf1c8084-ba50-4ce7-9439-34653001fc3b",
|
||||
"Domain": "test.com",
|
||||
"DomainCategory": "private",
|
||||
"IsDomainPrimaryAccount": true,
|
||||
"Settings": {
|
||||
"PeerLoginExpirationEnabled": true,
|
||||
"PeerLoginExpiration": 3600000000000
|
||||
},
|
||||
"SetupKeys": {
|
||||
"A2C8E62B-38F5-4553-B31E-DD66C696CEBB": {
|
||||
"Key": "A2C8E62B-38F5-4553-B31E-DD66C696CEBB",
|
||||
"Name": "Default key",
|
||||
"Type": "reusable",
|
||||
"CreatedAt": "2021-08-19T20:46:20.005936822+02:00",
|
||||
"ExpiresAt": "2321-09-18T20:46:20.005936822+02:00",
|
||||
"Revoked": false,
|
||||
"UsedTimes": 0
|
||||
|
||||
}
|
||||
},
|
||||
"Network": {
|
||||
"Id": "af1c8024-ha40-4ce2-9418-34653101fc3c",
|
||||
"Net": {
|
||||
"IP": "100.64.0.0",
|
||||
"Mask": "//8AAA=="
|
||||
},
|
||||
"Dns": null
|
||||
},
|
||||
"Peers": {
|
||||
"cfvprsrlo1hqoo49ohog": {
|
||||
"ID": "cfvprsrlo1hqoo49ohog",
|
||||
"Key": "5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=",
|
||||
"SetupKey": "72546A29-6BC8-4311-BCFC-9CDBF33F1A48",
|
||||
"IP": "100.64.114.31",
|
||||
"Meta": {
|
||||
"Hostname": "f2a34f6a4731",
|
||||
"GoOS": "linux",
|
||||
"Kernel": "Linux",
|
||||
"Core": "11",
|
||||
"Platform": "unknown",
|
||||
"OS": "Debian GNU/Linux",
|
||||
"WtVersion": "0.12.0",
|
||||
"UIVersion": ""
|
||||
},
|
||||
"Name": "f2a34f6a4731",
|
||||
"DNSLabel": "f2a34f6a4731",
|
||||
"Status": {
|
||||
"LastSeen": "2023-03-02T09:21:02.189035775+01:00",
|
||||
"Connected": false,
|
||||
"LoginExpired": false
|
||||
},
|
||||
"UserID": "",
|
||||
"SSHKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk",
|
||||
"SSHEnabled": false,
|
||||
"LoginExpirationEnabled": true,
|
||||
"LastLogin": "2023-03-01T19:48:19.817799698+01:00"
|
||||
},
|
||||
"cg05lnblo1hkg2j514p0": {
|
||||
"ID": "cg05lnblo1hkg2j514p0",
|
||||
"Key": "RlSy2vzoG2HyMBTUImXOiVhCBiiBa5qD5xzMxkiFDW4=",
|
||||
"SetupKey": "",
|
||||
"IP": "100.64.39.54",
|
||||
"Meta": {
|
||||
"Hostname": "expiredhost",
|
||||
"GoOS": "linux",
|
||||
"Kernel": "Linux",
|
||||
"Core": "22.04",
|
||||
"Platform": "x86_64",
|
||||
"OS": "Ubuntu",
|
||||
"WtVersion": "development",
|
||||
"UIVersion": ""
|
||||
},
|
||||
"Name": "expiredhost",
|
||||
"DNSLabel": "expiredhost",
|
||||
"Status": {
|
||||
"LastSeen": "2023-03-02T09:19:57.276717255+01:00",
|
||||
"Connected": false,
|
||||
"LoginExpired": true
|
||||
},
|
||||
"UserID": "edafee4e-63fb-11ec-90d6-0242ac120003",
|
||||
"SSHKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMbK5ZXJsGOOWoBT4OmkPtgdPZe2Q7bDuS/zjn2CZxhK",
|
||||
"SSHEnabled": false,
|
||||
"LoginExpirationEnabled": true,
|
||||
"LastLogin": "2023-03-02T09:14:21.791679181+01:00"
|
||||
},
|
||||
"cg3161rlo1hs9cq94gdg": {
|
||||
"ID": "cg3161rlo1hs9cq94gdg",
|
||||
"Key": "mVABSKj28gv+JRsf7e0NEGKgSOGTfU/nPB2cpuG56HU=",
|
||||
"SetupKey": "",
|
||||
"IP": "100.64.117.96",
|
||||
"Meta": {
|
||||
"Hostname": "testhost",
|
||||
"GoOS": "linux",
|
||||
"Kernel": "Linux",
|
||||
"Core": "22.04",
|
||||
"Platform": "x86_64",
|
||||
"OS": "Ubuntu",
|
||||
"WtVersion": "development",
|
||||
"UIVersion": ""
|
||||
},
|
||||
"Name": "testhost",
|
||||
"DNSLabel": "testhost",
|
||||
"Status": {
|
||||
"LastSeen": "2023-03-06T18:21:27.252010027+01:00",
|
||||
"Connected": false,
|
||||
"LoginExpired": false
|
||||
},
|
||||
"UserID": "edafee4e-63fb-11ec-90d6-0242ac120003",
|
||||
"SSHKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINWvvUkFFcrj48CWTkNUb/do/n52i1L5dH4DhGu+4ZuM",
|
||||
"SSHEnabled": false,
|
||||
"LoginExpirationEnabled": false,
|
||||
"LastLogin": "2023-03-07T09:02:47.442857106+01:00"
|
||||
}
|
||||
},
|
||||
"Users": {
|
||||
"edafee4e-63fb-11ec-90d6-0242ac120003": {
|
||||
"Id": "edafee4e-63fb-11ec-90d6-0242ac120003",
|
||||
"Role": "admin"
|
||||
},
|
||||
"f4f6d672-63fb-11ec-90d6-0242ac120003": {
|
||||
"Id": "f4f6d672-63fb-11ec-90d6-0242ac120003",
|
||||
"Role": "user"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user