[client, management] Add new network concept (#3047)

---------

Co-authored-by: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com>
Co-authored-by: bcmmbaga <bethuelmbaga12@gmail.com>
Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com>
This commit is contained in:
Viktor Liu
2024-12-20 11:30:28 +01:00
committed by GitHub
parent 37ad370344
commit ddc365f7a0
155 changed files with 13909 additions and 4993 deletions

View File

@@ -0,0 +1,383 @@
package resources
import (
"context"
"errors"
"fmt"
s "github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/groups"
"github.com/netbirdio/netbird/management/server/networks/resources/types"
"github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/store"
nbtypes "github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/util"
)
type Manager interface {
GetAllResourcesInNetwork(ctx context.Context, accountID, userID, networkID string) ([]*types.NetworkResource, error)
GetAllResourcesInAccount(ctx context.Context, accountID, userID string) ([]*types.NetworkResource, error)
GetAllResourceIDsInAccount(ctx context.Context, accountID, userID string) (map[string][]string, error)
CreateResource(ctx context.Context, userID string, resource *types.NetworkResource) (*types.NetworkResource, error)
GetResource(ctx context.Context, accountID, userID, networkID, resourceID string) (*types.NetworkResource, error)
UpdateResource(ctx context.Context, userID string, resource *types.NetworkResource) (*types.NetworkResource, error)
DeleteResource(ctx context.Context, accountID, userID, networkID, resourceID string) error
DeleteResourceInTransaction(ctx context.Context, transaction store.Store, accountID, userID, networkID, resourceID string) ([]func(), error)
}
type managerImpl struct {
store store.Store
permissionsManager permissions.Manager
groupsManager groups.Manager
accountManager s.AccountManager
}
func NewManager(store store.Store, permissionsManager permissions.Manager, groupsManager groups.Manager, accountManager s.AccountManager) Manager {
return &managerImpl{
store: store,
permissionsManager: permissionsManager,
groupsManager: groupsManager,
accountManager: accountManager,
}
}
func (m *managerImpl) GetAllResourcesInNetwork(ctx context.Context, accountID, userID, networkID string) ([]*types.NetworkResource, error) {
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, permissions.Networks, permissions.Read)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
if !ok {
return nil, status.NewPermissionDeniedError()
}
return m.store.GetNetworkResourcesByNetID(ctx, store.LockingStrengthShare, accountID, networkID)
}
func (m *managerImpl) GetAllResourcesInAccount(ctx context.Context, accountID, userID string) ([]*types.NetworkResource, error) {
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, permissions.Networks, permissions.Read)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
if !ok {
return nil, status.NewPermissionDeniedError()
}
return m.store.GetNetworkResourcesByAccountID(ctx, store.LockingStrengthShare, accountID)
}
func (m *managerImpl) GetAllResourceIDsInAccount(ctx context.Context, accountID, userID string) (map[string][]string, error) {
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, permissions.Networks, permissions.Read)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
if !ok {
return nil, status.NewPermissionDeniedError()
}
resources, err := m.store.GetNetworkResourcesByAccountID(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, fmt.Errorf("failed to get network resources: %w", err)
}
resourceMap := make(map[string][]string)
for _, resource := range resources {
resourceMap[resource.NetworkID] = append(resourceMap[resource.NetworkID], resource.ID)
}
return resourceMap, nil
}
func (m *managerImpl) CreateResource(ctx context.Context, userID string, resource *types.NetworkResource) (*types.NetworkResource, error) {
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, resource.AccountID, userID, permissions.Networks, permissions.Write)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
if !ok {
return nil, status.NewPermissionDeniedError()
}
resource, err = types.NewNetworkResource(resource.AccountID, resource.NetworkID, resource.Name, resource.Description, resource.Address, resource.GroupIDs)
if err != nil {
return nil, fmt.Errorf("failed to create new network resource: %w", err)
}
unlock := m.store.AcquireWriteLockByUID(ctx, resource.AccountID)
defer unlock()
var eventsToStore []func()
err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
_, err = transaction.GetNetworkResourceByName(ctx, store.LockingStrengthShare, resource.AccountID, resource.Name)
if err == nil {
return errors.New("resource already exists")
}
network, err := transaction.GetNetworkByID(ctx, store.LockingStrengthUpdate, resource.AccountID, resource.NetworkID)
if err != nil {
return fmt.Errorf("failed to get network: %w", err)
}
err = transaction.SaveNetworkResource(ctx, store.LockingStrengthUpdate, resource)
if err != nil {
return fmt.Errorf("failed to save network resource: %w", err)
}
event := func() {
m.accountManager.StoreEvent(ctx, userID, resource.ID, resource.AccountID, activity.NetworkResourceCreated, resource.EventMeta(network))
}
eventsToStore = append(eventsToStore, event)
res := nbtypes.Resource{
ID: resource.ID,
Type: resource.Type.String(),
}
for _, groupID := range resource.GroupIDs {
event, err := m.groupsManager.AddResourceToGroupInTransaction(ctx, transaction, resource.AccountID, userID, groupID, &res)
if err != nil {
return fmt.Errorf("failed to add resource to group: %w", err)
}
eventsToStore = append(eventsToStore, event)
}
err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, resource.AccountID)
if err != nil {
return fmt.Errorf("failed to increment network serial: %w", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to create network resource: %w", err)
}
for _, event := range eventsToStore {
event()
}
go m.accountManager.UpdateAccountPeers(ctx, resource.AccountID)
return resource, nil
}
func (m *managerImpl) GetResource(ctx context.Context, accountID, userID, networkID, resourceID string) (*types.NetworkResource, error) {
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, permissions.Networks, permissions.Read)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
if !ok {
return nil, status.NewPermissionDeniedError()
}
resource, err := m.store.GetNetworkResourceByID(ctx, store.LockingStrengthShare, accountID, resourceID)
if err != nil {
return nil, fmt.Errorf("failed to get network resource: %w", err)
}
if resource.NetworkID != networkID {
return nil, errors.New("resource not part of network")
}
return resource, nil
}
func (m *managerImpl) UpdateResource(ctx context.Context, userID string, resource *types.NetworkResource) (*types.NetworkResource, error) {
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, resource.AccountID, userID, permissions.Networks, permissions.Write)
if err != nil {
return nil, status.NewPermissionValidationError(err)
}
if !ok {
return nil, status.NewPermissionDeniedError()
}
resourceType, domain, prefix, err := types.GetResourceType(resource.Address)
if err != nil {
return nil, fmt.Errorf("failed to get resource type: %w", err)
}
resource.Type = resourceType
resource.Domain = domain
resource.Prefix = prefix
unlock := m.store.AcquireWriteLockByUID(ctx, resource.AccountID)
defer unlock()
var eventsToStore []func()
err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
network, err := transaction.GetNetworkByID(ctx, store.LockingStrengthUpdate, resource.AccountID, resource.NetworkID)
if err != nil {
return fmt.Errorf("failed to get network: %w", err)
}
if network.ID != resource.NetworkID {
return status.NewResourceNotPartOfNetworkError(resource.ID, resource.NetworkID)
}
_, err = transaction.GetNetworkResourceByID(ctx, store.LockingStrengthShare, resource.AccountID, resource.ID)
if err != nil {
return fmt.Errorf("failed to get network resource: %w", err)
}
oldResource, err := transaction.GetNetworkResourceByName(ctx, store.LockingStrengthShare, resource.AccountID, resource.Name)
if err == nil && oldResource.ID != resource.ID {
return errors.New("new resource name already exists")
}
oldResource, err = transaction.GetNetworkResourceByID(ctx, store.LockingStrengthShare, resource.AccountID, resource.ID)
if err != nil {
return fmt.Errorf("failed to get network resource: %w", err)
}
err = transaction.SaveNetworkResource(ctx, store.LockingStrengthUpdate, resource)
if err != nil {
return fmt.Errorf("failed to save network resource: %w", err)
}
events, err := m.updateResourceGroups(ctx, transaction, userID, resource, oldResource)
if err != nil {
return fmt.Errorf("failed to update resource groups: %w", err)
}
eventsToStore = append(eventsToStore, events...)
eventsToStore = append(eventsToStore, func() {
m.accountManager.StoreEvent(ctx, userID, resource.ID, resource.AccountID, activity.NetworkResourceUpdated, resource.EventMeta(network))
})
err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, resource.AccountID)
if err != nil {
return fmt.Errorf("failed to increment network serial: %w", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to update network resource: %w", err)
}
for _, event := range eventsToStore {
event()
}
go m.accountManager.UpdateAccountPeers(ctx, resource.AccountID)
return resource, nil
}
func (m *managerImpl) updateResourceGroups(ctx context.Context, transaction store.Store, userID string, newResource, oldResource *types.NetworkResource) ([]func(), error) {
res := nbtypes.Resource{
ID: newResource.ID,
Type: newResource.Type.String(),
}
oldResourceGroups, err := m.groupsManager.GetResourceGroupsInTransaction(ctx, transaction, store.LockingStrengthUpdate, oldResource.AccountID, oldResource.ID)
if err != nil {
return nil, fmt.Errorf("failed to get resource groups: %w", err)
}
oldGroupsIds := make([]string, 0)
for _, group := range oldResourceGroups {
oldGroupsIds = append(oldGroupsIds, group.ID)
}
var eventsToStore []func()
groupsToAdd := util.Difference(newResource.GroupIDs, oldGroupsIds)
for _, groupID := range groupsToAdd {
events, err := m.groupsManager.AddResourceToGroupInTransaction(ctx, transaction, newResource.AccountID, userID, groupID, &res)
if err != nil {
return nil, fmt.Errorf("failed to add resource to group: %w", err)
}
eventsToStore = append(eventsToStore, events)
}
groupsToRemove := util.Difference(oldGroupsIds, newResource.GroupIDs)
for _, groupID := range groupsToRemove {
events, err := m.groupsManager.RemoveResourceFromGroupInTransaction(ctx, transaction, newResource.AccountID, userID, groupID, res.ID)
if err != nil {
return nil, fmt.Errorf("failed to add resource to group: %w", err)
}
eventsToStore = append(eventsToStore, events)
}
return eventsToStore, nil
}
func (m *managerImpl) DeleteResource(ctx context.Context, accountID, userID, networkID, resourceID string) error {
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, permissions.Networks, permissions.Write)
if err != nil {
return status.NewPermissionValidationError(err)
}
if !ok {
return status.NewPermissionDeniedError()
}
unlock := m.store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
var events []func()
err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
events, err = m.DeleteResourceInTransaction(ctx, transaction, accountID, userID, networkID, resourceID)
if err != nil {
return fmt.Errorf("failed to delete resource: %w", err)
}
err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID)
if err != nil {
return fmt.Errorf("failed to increment network serial: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("failed to delete network resource: %w", err)
}
for _, event := range events {
event()
}
go m.accountManager.UpdateAccountPeers(ctx, accountID)
return nil
}
func (m *managerImpl) DeleteResourceInTransaction(ctx context.Context, transaction store.Store, accountID, userID, networkID, resourceID string) ([]func(), error) {
resource, err := transaction.GetNetworkResourceByID(ctx, store.LockingStrengthUpdate, accountID, resourceID)
if err != nil {
return nil, fmt.Errorf("failed to get network resource: %w", err)
}
network, err := transaction.GetNetworkByID(ctx, store.LockingStrengthUpdate, accountID, networkID)
if err != nil {
return nil, fmt.Errorf("failed to get network: %w", err)
}
if resource.NetworkID != networkID {
return nil, errors.New("resource not part of network")
}
groups, err := m.groupsManager.GetResourceGroupsInTransaction(ctx, transaction, store.LockingStrengthUpdate, accountID, resourceID)
if err != nil {
return nil, fmt.Errorf("failed to get resource groups: %w", err)
}
var eventsToStore []func()
for _, group := range groups {
event, err := m.groupsManager.RemoveResourceFromGroupInTransaction(ctx, transaction, accountID, userID, group.ID, resourceID)
if err != nil {
return nil, fmt.Errorf("failed to remove resource from group: %w", err)
}
eventsToStore = append(eventsToStore, event)
}
err = transaction.DeleteNetworkResource(ctx, store.LockingStrengthUpdate, accountID, resourceID)
if err != nil {
return nil, fmt.Errorf("failed to delete network resource: %w", err)
}
eventsToStore = append(eventsToStore, func() {
m.accountManager.StoreEvent(ctx, userID, resourceID, accountID, activity.NetworkResourceDeleted, resource.EventMeta(network))
})
return eventsToStore, nil
}

View File

@@ -0,0 +1,411 @@
package resources
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/management/server/groups"
"github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/networks/resources/types"
"github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/store"
)
func Test_GetAllResourcesInNetworkReturnsResources(t *testing.T) {
ctx := context.Background()
accountID := "testAccountId"
userID := "allowedUser"
networkID := "testNetworkId"
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
resources, err := manager.GetAllResourcesInNetwork(ctx, accountID, userID, networkID)
require.NoError(t, err)
require.Len(t, resources, 2)
}
func Test_GetAllResourcesInNetworkReturnsPermissionDenied(t *testing.T) {
ctx := context.Background()
accountID := "testAccountId"
userID := "invalidUser"
networkID := "testNetworkId"
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
resources, err := manager.GetAllResourcesInNetwork(ctx, accountID, userID, networkID)
require.Error(t, err)
require.Equal(t, status.NewPermissionDeniedError(), err)
require.Nil(t, resources)
}
func Test_GetAllResourcesInAccountReturnsResources(t *testing.T) {
ctx := context.Background()
accountID := "testAccountId"
userID := "allowedUser"
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
resources, err := manager.GetAllResourcesInAccount(ctx, accountID, userID)
require.NoError(t, err)
require.Len(t, resources, 2)
}
func Test_GetAllResourcesInAccountReturnsPermissionDenied(t *testing.T) {
ctx := context.Background()
accountID := "testAccountId"
userID := "invalidUser"
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
resources, err := manager.GetAllResourcesInAccount(ctx, accountID, userID)
require.Error(t, err)
require.Equal(t, status.NewPermissionDeniedError(), err)
require.Nil(t, resources)
}
func Test_GetResourceInNetworkReturnsResources(t *testing.T) {
ctx := context.Background()
accountID := "testAccountId"
userID := "allowedUser"
networkID := "testNetworkId"
resourceID := "testResourceId"
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
resource, err := manager.GetResource(ctx, accountID, userID, networkID, resourceID)
require.NoError(t, err)
require.Equal(t, resourceID, resource.ID)
}
func Test_GetResourceInNetworkReturnsPermissionDenied(t *testing.T) {
ctx := context.Background()
accountID := "testAccountId"
userID := "invalidUser"
networkID := "testNetworkId"
resourceID := "testResourceId"
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
resources, err := manager.GetResource(ctx, accountID, userID, networkID, resourceID)
require.Error(t, err)
require.Equal(t, status.NewPermissionDeniedError(), err)
require.Nil(t, resources)
}
func Test_CreateResourceSuccessfully(t *testing.T) {
ctx := context.Background()
userID := "allowedUser"
resource := &types.NetworkResource{
AccountID: "testAccountId",
NetworkID: "testNetworkId",
Name: "newResourceId",
Description: "description",
Address: "192.168.1.1",
}
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
createdResource, err := manager.CreateResource(ctx, userID, resource)
require.NoError(t, err)
require.Equal(t, resource.Name, createdResource.Name)
}
func Test_CreateResourceFailsWithPermissionDenied(t *testing.T) {
ctx := context.Background()
userID := "invalidUser"
resource := &types.NetworkResource{
AccountID: "testAccountId",
NetworkID: "testNetworkId",
Name: "testResourceId",
Description: "description",
Address: "192.168.1.1",
}
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
createdResource, err := manager.CreateResource(ctx, userID, resource)
require.Error(t, err)
require.Equal(t, status.NewPermissionDeniedError(), err)
require.Nil(t, createdResource)
}
func Test_CreateResourceFailsWithInvalidAddress(t *testing.T) {
ctx := context.Background()
userID := "allowedUser"
resource := &types.NetworkResource{
AccountID: "testAccountId",
NetworkID: "testNetworkId",
Name: "testResourceId",
Description: "description",
Address: "invalid-address",
}
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
createdResource, err := manager.CreateResource(ctx, userID, resource)
require.Error(t, err)
require.Nil(t, createdResource)
}
func Test_CreateResourceFailsWithUsedName(t *testing.T) {
ctx := context.Background()
userID := "allowedUser"
resource := &types.NetworkResource{
AccountID: "testAccountId",
NetworkID: "testNetworkId",
Name: "testResourceId",
Description: "description",
Address: "invalid-address",
}
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
createdResource, err := manager.CreateResource(ctx, userID, resource)
require.Error(t, err)
require.Nil(t, createdResource)
}
func Test_UpdateResourceSuccessfully(t *testing.T) {
ctx := context.Background()
accountID := "testAccountId"
userID := "allowedUser"
networkID := "testNetworkId"
resourceID := "testResourceId"
resource := &types.NetworkResource{
AccountID: accountID,
NetworkID: networkID,
Name: "someNewName",
ID: resourceID,
Description: "new-description",
Address: "1.2.3.0/24",
}
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
updatedResource, err := manager.UpdateResource(ctx, userID, resource)
require.NoError(t, err)
require.NotNil(t, updatedResource)
require.Equal(t, "new-description", updatedResource.Description)
require.Equal(t, "1.2.3.0/24", updatedResource.Address)
require.Equal(t, types.NetworkResourceType("subnet"), updatedResource.Type)
}
func Test_UpdateResourceFailsWithResourceNotFound(t *testing.T) {
ctx := context.Background()
accountID := "testAccountId"
userID := "allowedUser"
networkID := "testNetworkId"
resourceID := "otherResourceId"
resource := &types.NetworkResource{
AccountID: accountID,
NetworkID: networkID,
Name: resourceID,
Description: "new-description",
Address: "1.2.3.0/24",
}
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
updatedResource, err := manager.UpdateResource(ctx, userID, resource)
require.Error(t, err)
require.Nil(t, updatedResource)
}
func Test_UpdateResourceFailsWithNameInUse(t *testing.T) {
ctx := context.Background()
accountID := "testAccountId"
userID := "allowedUser"
networkID := "testNetworkId"
resourceID := "testResourceId"
resource := &types.NetworkResource{
AccountID: accountID,
NetworkID: networkID,
ID: resourceID,
Name: "used-name",
Description: "new-description",
Address: "1.2.3.0/24",
}
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
updatedResource, err := manager.UpdateResource(ctx, userID, resource)
require.Error(t, err)
require.Nil(t, updatedResource)
}
func Test_UpdateResourceFailsWithPermissionDenied(t *testing.T) {
ctx := context.Background()
accountID := "testAccountId"
userID := "invalidUser"
networkID := "testNetworkId"
resourceID := "testResourceId"
resource := &types.NetworkResource{
AccountID: accountID,
NetworkID: networkID,
Name: resourceID,
Description: "new-description",
Address: "1.2.3.0/24",
}
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
updatedResource, err := manager.UpdateResource(ctx, userID, resource)
require.Error(t, err)
require.Nil(t, updatedResource)
}
func Test_DeleteResourceSuccessfully(t *testing.T) {
ctx := context.Background()
accountID := "testAccountId"
userID := "allowedUser"
networkID := "testNetworkId"
resourceID := "testResourceId"
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
err = manager.DeleteResource(ctx, accountID, userID, networkID, resourceID)
require.NoError(t, err)
}
func Test_DeleteResourceFailsWithPermissionDenied(t *testing.T) {
ctx := context.Background()
accountID := "testAccountId"
userID := "invalidUser"
networkID := "testNetworkId"
resourceID := "testResourceId"
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(cleanUp)
permissionsManager := permissions.NewManagerMock()
am := mock_server.MockAccountManager{}
groupsManager := groups.NewManagerMock()
manager := NewManager(store, permissionsManager, groupsManager, &am)
err = manager.DeleteResource(ctx, accountID, userID, networkID, resourceID)
require.Error(t, err)
}

View File

@@ -0,0 +1,169 @@
package types
import (
"errors"
"fmt"
"net/netip"
"regexp"
"github.com/rs/xid"
nbDomain "github.com/netbirdio/netbird/management/domain"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/management/server/http/api"
)
type NetworkResourceType string
const (
host NetworkResourceType = "host"
subnet NetworkResourceType = "subnet"
domain NetworkResourceType = "domain"
)
func (p NetworkResourceType) String() string {
return string(p)
}
type NetworkResource struct {
ID string `gorm:"index"`
NetworkID string `gorm:"index"`
AccountID string `gorm:"index"`
Name string
Description string
Type NetworkResourceType
Address string `gorm:"-"`
GroupIDs []string `gorm:"-"`
Domain string
Prefix netip.Prefix `gorm:"serializer:json"`
}
func NewNetworkResource(accountID, networkID, name, description, address string, groupIDs []string) (*NetworkResource, error) {
resourceType, domain, prefix, err := GetResourceType(address)
if err != nil {
return nil, fmt.Errorf("invalid address: %w", err)
}
return &NetworkResource{
ID: xid.New().String(),
AccountID: accountID,
NetworkID: networkID,
Name: name,
Description: description,
Type: resourceType,
Address: address,
Domain: domain,
Prefix: prefix,
GroupIDs: groupIDs,
}, nil
}
func (n *NetworkResource) ToAPIResponse(groups []api.GroupMinimum) *api.NetworkResource {
addr := n.Prefix.String()
if n.Type == domain {
addr = n.Domain
}
return &api.NetworkResource{
Id: n.ID,
Name: n.Name,
Description: &n.Description,
Type: api.NetworkResourceType(n.Type.String()),
Address: addr,
Groups: groups,
}
}
func (n *NetworkResource) FromAPIRequest(req *api.NetworkResourceRequest) {
n.Name = req.Name
if req.Description != nil {
n.Description = *req.Description
}
n.Address = req.Address
n.GroupIDs = req.Groups
}
func (n *NetworkResource) Copy() *NetworkResource {
return &NetworkResource{
ID: n.ID,
AccountID: n.AccountID,
NetworkID: n.NetworkID,
Name: n.Name,
Description: n.Description,
Type: n.Type,
Address: n.Address,
Domain: n.Domain,
Prefix: n.Prefix,
GroupIDs: n.GroupIDs,
}
}
func (n *NetworkResource) ToRoute(peer *nbpeer.Peer, router *routerTypes.NetworkRouter) *route.Route {
r := &route.Route{
ID: route.ID(fmt.Sprintf("%s:%s", n.ID, peer.ID)),
AccountID: n.AccountID,
KeepRoute: true,
NetID: route.NetID(n.Name),
Description: n.Description,
Peer: peer.Key,
PeerGroups: nil,
Masquerade: router.Masquerade,
Metric: router.Metric,
Enabled: true,
Groups: nil,
AccessControlGroups: nil,
}
if n.Type == host || n.Type == subnet {
r.Network = n.Prefix
r.NetworkType = route.IPv4Network
if n.Prefix.Addr().Is6() {
r.NetworkType = route.IPv6Network
}
}
if n.Type == domain {
domainList, err := nbDomain.FromStringList([]string{n.Domain})
if err != nil {
return nil
}
r.Domains = domainList
r.NetworkType = route.DomainNetwork
// add default placeholder for domain network
r.Network = netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 0, 2, 0}), 32)
}
return r
}
func (n *NetworkResource) EventMeta(network *networkTypes.Network) map[string]any {
return map[string]any{"name": n.Name, "type": n.Type, "network_name": network.Name, "network_id": network.ID}
}
// GetResourceType returns the type of the resource based on the address
func GetResourceType(address string) (NetworkResourceType, string, netip.Prefix, error) {
if prefix, err := netip.ParsePrefix(address); err == nil {
if prefix.Bits() == 32 || prefix.Bits() == 128 {
return host, "", prefix, nil
}
return subnet, "", prefix, nil
}
if ip, err := netip.ParseAddr(address); err == nil {
return host, "", netip.PrefixFrom(ip, ip.BitLen()), nil
}
domainRegex := regexp.MustCompile(`^(\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$`)
if domainRegex.MatchString(address) {
return domain, address, netip.Prefix{}, nil
}
return "", "", netip.Prefix{}, errors.New("not a valid host, subnet, or domain")
}

View File

@@ -0,0 +1,53 @@
package types
import (
"net/netip"
"testing"
)
func TestGetResourceType(t *testing.T) {
tests := []struct {
input string
expectedType NetworkResourceType
expectedErr bool
expectedDomain string
expectedPrefix netip.Prefix
}{
// Valid host IPs
{"1.1.1.1", host, false, "", netip.MustParsePrefix("1.1.1.1/32")},
{"1.1.1.1/32", host, false, "", netip.MustParsePrefix("1.1.1.1/32")},
// Valid subnets
{"192.168.1.0/24", subnet, false, "", netip.MustParsePrefix("192.168.1.0/24")},
{"10.0.0.0/16", subnet, false, "", netip.MustParsePrefix("10.0.0.0/16")},
// Valid domains
{"example.com", domain, false, "example.com", netip.Prefix{}},
{"*.example.com", domain, false, "*.example.com", netip.Prefix{}},
{"sub.example.com", domain, false, "sub.example.com", netip.Prefix{}},
// Invalid inputs
{"invalid", "", true, "", netip.Prefix{}},
{"1.1.1.1/abc", "", true, "", netip.Prefix{}},
{"1234", "", true, "", netip.Prefix{}},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
result, domain, prefix, err := GetResourceType(tt.input)
if result != tt.expectedType {
t.Errorf("Expected type %v, got %v", tt.expectedType, result)
}
if tt.expectedErr && err == nil {
t.Errorf("Expected error, got nil")
}
if prefix != tt.expectedPrefix {
t.Errorf("Expected address %v, got %v", tt.expectedPrefix, prefix)
}
if domain != tt.expectedDomain {
t.Errorf("Expected domain %v, got %v", tt.expectedDomain, domain)
}
})
}
}