diff --git a/go.mod b/go.mod index 8aad61e..e1152da 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/stretchr/testify v1.10.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 google.golang.org/grpc v1.67.3 + gopkg.in/natefinch/lumberjack.v2 v2.0.0 gorm.io/driver/postgres v1.5.11 gorm.io/driver/sqlite v1.5.7 gorm.io/gorm v1.25.12 @@ -166,7 +167,6 @@ require ( google.golang.org/api v0.215.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect google.golang.org/protobuf v1.36.1 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/mysql v1.5.7 // indirect diff --git a/internal/controllers/network_map/controller.go b/internal/controllers/network_map/controller.go index 327053e..58649e7 100644 --- a/internal/controllers/network_map/controller.go +++ b/internal/controllers/network_map/controller.go @@ -1,4 +1,35 @@ package network_map +import ( + "fmt" + + log "github.com/sirupsen/logrus" + + "management/internal/shared/db" + appmetrics "management/internal/shared/metrics" +) + type Controller struct { + repo Repository + metrics *metrics +} + +func NewController(store *db.Store, metrics *appmetrics.AppMetrics) *Controller { + cMetrics, err := appmetrics.RegisterMetrics(metrics, newMetrics) + if err != nil { + log.Fatalf("Failed to register app metrics: %v", err) + } + return &Controller{ + repo: newRepository(store, cMetrics), + metrics: cMetrics, + } +} + +func (c *Controller) UpdatePeers(accountID string) error { + data, err := c.repo.GetNetworkMapData(accountID) + if err != nil { + return fmt.Errorf("get network map data: %w", err) + } + + return nil } diff --git a/internal/controllers/network_map/metrics.go b/internal/controllers/network_map/metrics.go new file mode 100644 index 0000000..1143540 --- /dev/null +++ b/internal/controllers/network_map/metrics.go @@ -0,0 +1,31 @@ +package network_map + +import ( + "context" + "time" + + "go.opentelemetry.io/otel/metric" +) + +type metrics struct { + dbAccessDuration metric.Int64Histogram +} + +func newMetrics(meter metric.Meter) (*metrics, error) { + dbAccessDuration, err := meter.Int64Histogram( + "sync_request_duration_seconds", + metric.WithDescription("Duration of sync requests in seconds"), + metric.WithUnit("s"), + ) + if err != nil { + return nil, err + } + + return &metrics{ + dbAccessDuration: dbAccessDuration, + }, nil +} + +func (m *metrics) RecordDBAccessDuration(duration time.Duration) { + m.dbAccessDuration.Record(context.Background(), duration.Milliseconds(), metric.WithAttributes()) +} diff --git a/internal/controllers/network_map/repository.go b/internal/controllers/network_map/repository.go new file mode 100644 index 0000000..0ecbaac --- /dev/null +++ b/internal/controllers/network_map/repository.go @@ -0,0 +1,65 @@ +package network_map + +import ( + "time" + + nbpeer "github.com/netbirdio/netbird/management/server/peer" + + "management/internal/modules/accounts" + "management/internal/modules/groups" + "management/internal/modules/networks" + "management/internal/modules/networks/resources" + "management/internal/modules/networks/routers" + "management/internal/modules/policies" + "management/internal/shared/db" +) + +type NetworkMapData struct { + // we have to name column to aid as it collides with Network.Id when work with associations + Id string `gorm:"primaryKey"` + + Domain string `gorm:"index"` + DomainCategory string + IsDomainPrimaryAccount bool + Network *accounts.Network `gorm:"embedded;embeddedPrefix:network_"` + Peers []nbpeer.Peer `json:"-" gorm:"foreignKey:AccountID;references:id"` + Groups []groups.Group `json:"-" gorm:"foreignKey:AccountID;references:id"` + Policies []*policies.Policy `gorm:"foreignKey:AccountID;references:id"` + + Networks []*networks.Network `gorm:"foreignKey:AccountID;references:id"` + NetworkRouters []*routers.NetworkRouter `gorm:"foreignKey:AccountID;references:id"` + NetworkResources []*resources.NetworkResource `gorm:"foreignKey:AccountID;references:id"` +} + +type Repository interface { + GetNetworkMapData(accountID string) (*NetworkMapData, error) +} + +type repository struct { + store *db.Store + metrics *metrics +} + +func newRepository(s *db.Store, metrics *metrics) Repository { + return &repository{ + store: s, + metrics: metrics, + } +} + +func (r *repository) GetNetworkMapData(accountID string) (*NetworkMapData, error) { + start := time.Now() + var networkMapData NetworkMapData + err := r.store.GetOne(nil, db.LockingStrengthShare, &networkMapData, "id = ?", accountID) + if err != nil { + return nil, err + } + + if err := r.store.Load(&networkMapData, "Peers", "Groups", "Policies", "Networks", "NetworkRouters", "NetworkResources"); err != nil { + return nil, err + } + + r.metrics.RecordDBAccessDuration(time.Since(start)) + + return &networkMapData, nil +} diff --git a/internal/controllers/network_map/updatechannel.go b/internal/controllers/network_map/updatechannel.go index 1b40b3e..e804a01 100644 --- a/internal/controllers/network_map/updatechannel.go +++ b/internal/controllers/network_map/updatechannel.go @@ -1,4 +1,4 @@ -package server +package network_map import ( "context" @@ -15,8 +15,11 @@ import ( const channelBufferSize = 100 type UpdateMessage struct { - Update *proto.SyncResponse - NetworkMap *types.NetworkMap + Update *proto.SyncResponse + NetworkMap *types.NetworkMap + PeerManager *nbpeer.Manager + PolicyManager *nbpeer.PolicyManager + GroupManager *nbpeer.GroupManager } type UpdateChannel struct { diff --git a/internal/modules/accounts/manager.go b/internal/modules/accounts/manager.go index 12ef7cc..ec6c5a1 100644 --- a/internal/modules/accounts/manager.go +++ b/internal/modules/accounts/manager.go @@ -1,10 +1 @@ package accounts - -import "management/pkg/logging" - -var log = logging.LoggerForThisPackage() - -type Manager struct { - repo Repository - handler *handler -} diff --git a/internal/modules/accounts/api.go b/internal/modules/accounts/manager/api.go similarity index 99% rename from internal/modules/accounts/api.go rename to internal/modules/accounts/manager/api.go index 4acfaac..c8e1dd3 100644 --- a/internal/modules/accounts/api.go +++ b/internal/modules/accounts/manager/api.go @@ -1,4 +1,4 @@ -package accounts +package manager import ( "encoding/json" diff --git a/internal/modules/accounts/manager/manager.go b/internal/modules/accounts/manager/manager.go new file mode 100644 index 0000000..c07f9df --- /dev/null +++ b/internal/modules/accounts/manager/manager.go @@ -0,0 +1,10 @@ +package manager + +import "management/pkg/logging" + +var log = logging.LoggerForThisPackage() + +type Manager struct { + repo Repository + handler *handler +} diff --git a/internal/modules/accounts/repository.go b/internal/modules/accounts/manager/repository.go similarity index 95% rename from internal/modules/accounts/repository.go rename to internal/modules/accounts/manager/repository.go index 5057b8e..4f401a3 100644 --- a/internal/modules/accounts/repository.go +++ b/internal/modules/accounts/manager/repository.go @@ -1,4 +1,4 @@ -package accounts +package manager import "management/internal/shared/db" diff --git a/internal/modules/accounts/network.go b/internal/modules/accounts/network.go new file mode 100644 index 0000000..456e0a3 --- /dev/null +++ b/internal/modules/accounts/network.go @@ -0,0 +1,73 @@ +package accounts + +import ( + "math/rand" + "net" + "sync" + "time" + + "github.com/c-robinson/iplib" + "github.com/rs/xid" +) + +const ( + // SubnetSize is a size of the subnet of the global network, e.g. 100.77.0.0/16 + SubnetSize = 16 + // NetSize is a global network size 100.64.0.0/10 + NetSize = 10 + + // AllowedIPsFormat generates Wireguard AllowedIPs format (e.g. 100.64.30.1/32) + AllowedIPsFormat = "%s/32" +) + +type Network struct { + Identifier string `json:"id"` + Net net.IPNet `gorm:"serializer:json"` + Dns string + // Serial is an ID that increments by 1 when any change to the network happened (e.g. new peer has been added). + // Used to synchronize state to the client apps. + Serial uint64 + + Mu sync.Mutex `json:"-" gorm:"-"` +} + +// NewNetwork creates a new Network initializing it with a Serial=0 +// It takes a random /16 subnet from 100.64.0.0/10 (64 different subnets) +func NewNetwork() *Network { + + n := iplib.NewNet4(net.ParseIP("100.64.0.0"), NetSize) + sub, _ := n.Subnet(SubnetSize) + + s := rand.NewSource(time.Now().Unix()) + r := rand.New(s) + intn := r.Intn(len(sub)) + + return &Network{ + Identifier: xid.New().String(), + Net: sub[intn].IPNet, + Dns: "", + Serial: 0} +} + +// IncSerial increments Serial by 1 reflecting that the network state has been changed +func (n *Network) IncSerial() { + n.Mu.Lock() + defer n.Mu.Unlock() + n.Serial++ +} + +// CurrentSerial returns the Network.Serial of the network (latest state id) +func (n *Network) CurrentSerial() uint64 { + n.Mu.Lock() + defer n.Mu.Unlock() + return n.Serial +} + +func (n *Network) Copy() *Network { + return &Network{ + Identifier: n.Identifier, + Net: n.Net, + Dns: n.Dns, + Serial: n.Serial, + } +} diff --git a/internal/modules/accounts/settings/api.go b/internal/modules/accounts/settings/manager/api.go similarity index 99% rename from internal/modules/accounts/settings/api.go rename to internal/modules/accounts/settings/manager/api.go index 900c663..3c0050e 100644 --- a/internal/modules/accounts/settings/api.go +++ b/internal/modules/accounts/settings/manager/api.go @@ -1,4 +1,4 @@ -package settings +package manager import ( "encoding/json" diff --git a/internal/modules/accounts/settings/manager.go b/internal/modules/accounts/settings/manager/manager.go similarity index 99% rename from internal/modules/accounts/settings/manager.go rename to internal/modules/accounts/settings/manager/manager.go index aae981e..1527c3b 100644 --- a/internal/modules/accounts/settings/manager.go +++ b/internal/modules/accounts/settings/manager/manager.go @@ -1,4 +1,4 @@ -package settings +package manager import ( "context" diff --git a/internal/modules/accounts/settings/repository.go b/internal/modules/accounts/settings/manager/repository.go similarity index 98% rename from internal/modules/accounts/settings/repository.go rename to internal/modules/accounts/settings/manager/repository.go index 7d6d033..8320f9b 100644 --- a/internal/modules/accounts/settings/repository.go +++ b/internal/modules/accounts/settings/manager/repository.go @@ -1,4 +1,4 @@ -package settings +package manager import ( "management/internal/modules/accounts/settings/types" diff --git a/internal/modules/accounts/settings/types/settings.go b/internal/modules/accounts/settings/settings.go similarity index 99% rename from internal/modules/accounts/settings/types/settings.go rename to internal/modules/accounts/settings/settings.go index 7054ede..736be8f 100644 --- a/internal/modules/accounts/settings/types/settings.go +++ b/internal/modules/accounts/settings/settings.go @@ -1,4 +1,4 @@ -package types +package settings import ( "time" diff --git a/internal/modules/groups/group.go b/internal/modules/groups/group.go new file mode 100644 index 0000000..e2b31c9 --- /dev/null +++ b/internal/modules/groups/group.go @@ -0,0 +1,25 @@ +package groups + +import "github.com/netbirdio/netbird/management/server/integration_reference" + +type Group struct { + // ID of the group + ID string `gorm:"primaryKey"` + + // AccountID is a reference to Account that this object belongs + AccountID string `json:"-" gorm:"index"` + + // Name visible in the UI + Name string + + // Issued defines how this group was created (enum of "api", "integration" or "jwt") + Issued string + + // Peers list of the group + Peers []string `gorm:"serializer:json"` + + // Resources contains a list of resources in that group + Resources []Resource `gorm:"serializer:json"` + + IntegrationReference integration_reference.IntegrationReference `gorm:"embedded;embeddedPrefix:integration_ref_"` +} diff --git a/internal/modules/groups/api.go b/internal/modules/groups/manager/api.go similarity index 99% rename from internal/modules/groups/api.go rename to internal/modules/groups/manager/api.go index 992d699..7b46e7c 100644 --- a/internal/modules/groups/api.go +++ b/internal/modules/groups/manager/api.go @@ -1,4 +1,4 @@ -package groups +package manager import ( "encoding/json" diff --git a/internal/modules/groups/manager.go b/internal/modules/groups/manager/manager.go similarity index 99% rename from internal/modules/groups/manager.go rename to internal/modules/groups/manager/manager.go index f50710f..0ed2f58 100644 --- a/internal/modules/groups/manager.go +++ b/internal/modules/groups/manager/manager.go @@ -1,4 +1,4 @@ -package groups +package manager import ( "context" diff --git a/internal/modules/groups/repository.go b/internal/modules/groups/manager/repository.go similarity index 95% rename from internal/modules/groups/repository.go rename to internal/modules/groups/manager/repository.go index c4dc24f..f8e342f 100644 --- a/internal/modules/groups/repository.go +++ b/internal/modules/groups/manager/repository.go @@ -1,4 +1,4 @@ -package groups +package manager import ( "management/internal/shared/db" diff --git a/internal/modules/groups/resource.go b/internal/modules/groups/resource.go new file mode 100644 index 0000000..a43e94d --- /dev/null +++ b/internal/modules/groups/resource.go @@ -0,0 +1,28 @@ +package groups + +import "github.com/netbirdio/netbird/management/server/http/api" + +type Resource struct { + ID string + Type string +} + +func (r *Resource) ToAPIResponse() *api.Resource { + if r.ID == "" && r.Type == "" { + return nil + } + + return &api.Resource{ + Id: r.ID, + Type: api.ResourceType(r.Type), + } +} + +func (r *Resource) FromAPIRequest(req *api.Resource) { + if req == nil { + return + } + + r.ID = req.Id + r.Type = string(req.Type) +} diff --git a/internal/modules/networks/interface.go b/internal/modules/networks/interface.go new file mode 100644 index 0000000..17c47c8 --- /dev/null +++ b/internal/modules/networks/interface.go @@ -0,0 +1,19 @@ +package networks + +import ( + "context" + + "management/internal/shared/db" + "management/internal/shared/hook" +) + +type Manager interface { + GetAllNetworks(ctx context.Context, tx db.Transaction, strength db.LockingStrength, accountID, userID string) ([]*Network, error) + GetNetwork(ctx context.Context, tx db.Transaction, strength db.LockingStrength, accountID, userID, networkID string) (*Network, error) + CreateNetwork(ctx context.Context, tx db.Transaction, userID string, network *Network) (*Network, error) + UpdateNetwork(ctx context.Context, tx db.Transaction, userID string, network *Network) (*Network, error) + DeleteNetwork(ctx context.Context, tx db.Transaction, accountID, userID, networkID string) error + + // events + OnNetworkDelete() *hook.Hook[*NetworkEvent] +} diff --git a/internal/modules/networks/manager/api.go b/internal/modules/networks/manager/api.go new file mode 100644 index 0000000..f4bceac --- /dev/null +++ b/internal/modules/networks/manager/api.go @@ -0,0 +1,55 @@ +package manager + +import ( + "net/http" + + "github.com/gorilla/mux" + nbcontext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/management/server/http/util" + + "management/internal/modules/networks" + "management/internal/shared/errors" + "management/internal/shared/permissions" + "management/internal/shared/permissions/modules" + "management/internal/shared/permissions/operations" +) + +type handler struct { + manager networks.Manager + permissionsManager permissions.Manager +} + +func newHandler(manager networks.Manager, permissionsManager permissions.Manager) *handler { + return &handler{ + manager: manager, + permissionsManager: permissionsManager, + } +} + +func (h *handler) RegisterEndpoints(router *mux.Router) { + router.HandleFunc("/networks/{id}", h.deleteNetwork).Methods("DELETE", "OPTIONS") +} + +func (h *handler) deleteNetwork(w http.ResponseWriter, r *http.Request) { + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + allowed, err := h.permissionsManager.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, modules.Networks, operations.Write) + if err != nil { + util.WriteError(r.Context(), errors.NewPermissionValidationError(err), w) + return + } + if !allowed { + util.WriteError(r.Context(), errors.NewPermissionDeniedError(), w) + } + + err = h.manager.DeleteNetwork(r.Context(), nil, userAuth.AccountId, userAuth.UserId, mux.Vars(r)["id"]) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + // _ = json.NewEncoder(w).Encode(users) +} diff --git a/internal/modules/networks/manager/events.go b/internal/modules/networks/manager/events.go new file mode 100644 index 0000000..a512f67 --- /dev/null +++ b/internal/modules/networks/manager/events.go @@ -0,0 +1,10 @@ +package manager + +import ( + "management/internal/modules/networks" + "management/internal/shared/hook" +) + +func (m *managerImpl) OnNetworkDelete() *hook.Hook[*networks.NetworkEvent] { + return m.onNetworkDelete +} diff --git a/internal/modules/networks/manager/manager.go b/internal/modules/networks/manager/manager.go new file mode 100644 index 0000000..3554c98 --- /dev/null +++ b/internal/modules/networks/manager/manager.go @@ -0,0 +1,90 @@ +package manager + +import ( + "context" + "fmt" + + "github.com/gorilla/mux" + "github.com/rs/xid" + + "management/internal/modules/networks" + "management/internal/shared/db" + "management/internal/shared/hook" + "management/internal/shared/permissions" +) + +type managerImpl struct { + repo Repository + + onNetworkDelete *hook.Hook[*networks.NetworkEvent] +} + +func NewManager(store *db.Store, router *mux.Router, permissionsManager permissions.Manager) networks.Manager { + repo := newRepository(store) + m := &managerImpl{ + repo: repo, + + onNetworkDelete: &hook.Hook[*networks.NetworkEvent]{}, + } + api := newHandler(m, permissionsManager) + api.RegisterEndpoints(router) + return m +} + +func (m *managerImpl) GetAllNetworks(ctx context.Context, tx db.Transaction, strength db.LockingStrength, accountID, userID string) ([]*networks.Network, error) { + return m.repo.GetAccountNetworks(tx, strength, accountID) +} + +func (m *managerImpl) CreateNetwork(ctx context.Context, tx db.Transaction, userID string, network *networks.Network) (*networks.Network, error) { + network.ID = xid.New().String() + + err := m.repo.CreateNetwork(tx, network) + if err != nil { + return nil, fmt.Errorf("failed to save network: %w", err) + } + + // m.accountManager.StoreEvent(ctx, userID, network.ID, network.AccountID, activity.NetworkCreated, network.EventMeta()) + + return network, nil +} + +func (m *managerImpl) GetNetwork(ctx context.Context, tx db.Transaction, strength db.LockingStrength, accountID, userID, networkID string) (*networks.Network, error) { + return m.repo.GetNetworkByID(tx, strength, accountID, networkID) +} + +func (m *managerImpl) UpdateNetwork(ctx context.Context, tx db.Transaction, userID string, network *networks.Network) (*networks.Network, error) { + _, err := m.repo.GetNetworkByID(tx, db.LockingStrengthUpdate, network.AccountID, network.ID) + if err != nil { + return nil, fmt.Errorf("failed to get network: %w", err) + } + + // m.accountManager.StoreEvent(ctx, userID, network.ID, network.AccountID, activity.NetworkUpdated, network.EventMeta()) + + return network, m.repo.UpdateNetwork(tx, network) +} + +func (m *managerImpl) DeleteNetwork(ctx context.Context, tx db.Transaction, accountID, userID, networkID string) error { + return db.WithTx(m.repo.Store(), tx, func(tx db.Transaction) error { + network := &networks.Network{ID: networkID} + + ev := &networks.NetworkEvent{ + Context: ctx, + Tx: tx, + Network: network, + } + + err := m.OnNetworkDelete().Trigger(ev, func(ne *networks.NetworkEvent) error { + if err := m.repo.DeleteNetwork(ne.Tx, ne.Network); err != nil { + return fmt.Errorf("failed to delete network: %w", err) + } + + tx.AddEvent(func() { + // addActivityEvent("Network deleted") + // noop + }) + return nil + }) + + return err + }) +} diff --git a/internal/modules/networks/manager/metrics.go b/internal/modules/networks/manager/metrics.go new file mode 100644 index 0000000..5d04392 --- /dev/null +++ b/internal/modules/networks/manager/metrics.go @@ -0,0 +1 @@ +package manager diff --git a/internal/modules/networks/manager/repository.go b/internal/modules/networks/manager/repository.go new file mode 100644 index 0000000..8e3e775 --- /dev/null +++ b/internal/modules/networks/manager/repository.go @@ -0,0 +1,61 @@ +package manager + +import ( + "management/internal/modules/networks" + "management/internal/shared/db" +) + +type Repository interface { + Store() *db.Store + CreateNetwork(tx db.Transaction, network *networks.Network) error + UpdateNetwork(tx db.Transaction, network *networks.Network) error + DeleteNetwork(tx db.Transaction, network *networks.Network) error + GetNetworkByID(tx db.Transaction, lockingStrength db.LockingStrength, accountID, networkID string) (*networks.Network, error) + GetAccountNetworks(tx db.Transaction, lockingStrength db.LockingStrength, accountID string) ([]*networks.Network, error) +} + +type repository struct { + store *db.Store +} + +func newRepository(s *db.Store) Repository { + return &repository{store: s} +} + +func (r *repository) RunInTx(fn func(tx db.Transaction) error) error { + return r.store.RunInTx(fn) +} + +func (r *repository) CreateNetwork(tx db.Transaction, network *networks.Network) error { + return r.store.Create(tx, network) +} + +func (r *repository) UpdateNetwork(tx db.Transaction, network *networks.Network) error { + return r.store.Update(tx, network) +} + +func (r *repository) DeleteNetwork(tx db.Transaction, network *networks.Network) error { + return r.store.Delete(tx, network) +} + +func (r *repository) GetNetworkByID(tx db.Transaction, lockingStrength db.LockingStrength, accountID, networkID string) (*networks.Network, error) { + var network networks.Network + err := r.store.GetOne(tx, lockingStrength, &network, "account_id = ? AND network_id = ?", accountID, networkID) + if err != nil { + return nil, err + } + return &network, nil +} + +func (r *repository) GetAccountNetworks(tx db.Transaction, lockingStrength db.LockingStrength, accountID string) ([]*networks.Network, error) { + var networks []*networks.Network + err := r.store.GetMany(tx, lockingStrength, &networks, "account_id = ?", accountID) + if err != nil { + return nil, err + } + return networks, nil +} + +func (r *repository) Store() *db.Store { + return r.store +} diff --git a/internal/modules/networks/network.go b/internal/modules/networks/network.go new file mode 100644 index 0000000..ac30245 --- /dev/null +++ b/internal/modules/networks/network.go @@ -0,0 +1,55 @@ +package networks + +import ( + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/rs/xid" +) + +type Network struct { + ID string + AccountID string + Name string + Description string +} + +func NewNetwork(accountId, name, description string) *Network { + return &Network{ + ID: xid.New().String(), + AccountID: accountId, + Name: name, + Description: description, + } +} + +func (n *Network) ToAPIResponse(routerIDs []string, resourceIDs []string, routingPeersCount int, policyIDs []string) *api.Network { + return &api.Network{ + Id: n.ID, + Name: n.Name, + Description: &n.Description, + Routers: routerIDs, + Resources: resourceIDs, + RoutingPeersCount: routingPeersCount, + Policies: policyIDs, + } +} + +func (n *Network) FromAPIRequest(req *api.NetworkRequest) { + n.Name = req.Name + if req.Description != nil { + n.Description = *req.Description + } +} + +// Copy returns a copy of a posture checks. +func (n *Network) Copy() *Network { + return &Network{ + ID: n.ID, + AccountID: n.AccountID, + Name: n.Name, + Description: n.Description, + } +} + +func (n *Network) EventMeta() map[string]any { + return map[string]any{"name": n.Name} +} diff --git a/internal/modules/networks/networkevent.go b/internal/modules/networks/networkevent.go new file mode 100644 index 0000000..55b31ae --- /dev/null +++ b/internal/modules/networks/networkevent.go @@ -0,0 +1,15 @@ +package networks + +import ( + "context" + "management/internal/shared/db" + "management/internal/shared/hook" +) + +type NetworkEvent struct { + hook.Event + + Context context.Context + Tx db.Transaction + Network *Network +} diff --git a/internal/modules/networks/resources/manager.go b/internal/modules/networks/resources/manager.go new file mode 100644 index 0000000..146b5e2 --- /dev/null +++ b/internal/modules/networks/resources/manager.go @@ -0,0 +1,21 @@ +package resources + +import ( + "context" + + "management/internal/modules/networks" + "management/internal/shared/db" +) + +type Manager interface { + // Create + + // Read + GetNetworkResourcesByNetID(ctx context.Context, tx db.Transaction, lockingStrength db.LockingStrength, network *networks.Network) ([]*NetworkResource, error) + + // Update + + // Delete + DeleteResource(ctx context.Context, tx db.Transaction, resource *NetworkResource) error + DeleteResourcesInNetwork(ctx context.Context, tx db.Transaction, network *networks.Network) error +} diff --git a/internal/modules/networks/resources/manager/api.go b/internal/modules/networks/resources/manager/api.go new file mode 100644 index 0000000..5d04392 --- /dev/null +++ b/internal/modules/networks/resources/manager/api.go @@ -0,0 +1 @@ +package manager diff --git a/internal/modules/networks/resources/manager/manager.go b/internal/modules/networks/resources/manager/manager.go new file mode 100644 index 0000000..f1e1a97 --- /dev/null +++ b/internal/modules/networks/resources/manager/manager.go @@ -0,0 +1,69 @@ +package manager + +import ( + "context" + "fmt" + + "github.com/gorilla/mux" + + "management/internal/modules/networks" + "management/internal/modules/networks/resources" + "management/internal/shared/db" +) + +type managerImpl struct { + repo Repository + networkManager networks.Manager +} + +func NewManager(store *db.Store, router *mux.Router, networkManager networks.Manager) resources.Manager { + repo := newRepository(store) + m := &managerImpl{ + repo: repo, + networkManager: networkManager, + } + + networkManager.OnNetworkDelete().BindFunc(func(e *networks.NetworkEvent) error { + if err := m.DeleteResourcesInNetwork(e.Context, e.Tx, e.Network); err != nil { + return fmt.Errorf("failed to delete resources in network: %w", err) + } + + return e.Next() + }) + + // api := newHandler(m, permissionsManager) + // api.RegisterEndpoints(router) + return m +} + +func (m *managerImpl) GetNetworkResourcesByNetID(ctx context.Context, tx db.Transaction, lockingStrength db.LockingStrength, network *networks.Network) ([]*resources.NetworkResource, error) { + return m.repo.GetResourcesByNetworkID(tx, lockingStrength, network.ID) +} + +func (m *managerImpl) DeleteResourcesInNetwork(ctx context.Context, tx db.Transaction, network *networks.Network) error { + resources, err := m.GetNetworkResourcesByNetID(ctx, tx, db.LockingStrengthUpdate, network) + if err != nil { + return err + } + + for _, resource := range resources { + err = m.DeleteResource(ctx, tx, resource) + if err != nil { + return err + } + } + + return nil +} + +func (m *managerImpl) DeleteResource(ctx context.Context, tx db.Transaction, resource *resources.NetworkResource) error { + if err := m.repo.DeleteResource(tx, resource); err != nil { + return fmt.Errorf("failed to delete network: %w", err) + } + + tx.AddEvent(func() { + // addActivityEvent("resource deleted") + // noop + }) + return nil +} diff --git a/internal/modules/networks/resources/manager/metrics.go b/internal/modules/networks/resources/manager/metrics.go new file mode 100644 index 0000000..5d04392 --- /dev/null +++ b/internal/modules/networks/resources/manager/metrics.go @@ -0,0 +1 @@ +package manager diff --git a/internal/modules/networks/resources/manager/repository.go b/internal/modules/networks/resources/manager/repository.go new file mode 100644 index 0000000..68ada59 --- /dev/null +++ b/internal/modules/networks/resources/manager/repository.go @@ -0,0 +1,37 @@ +package manager + +import ( + "management/internal/modules/networks/resources" + "management/internal/shared/db" +) + +type Repository interface { + Store() *db.Store + GetResourcesByNetworkID(tx db.Transaction, strength db.LockingStrength, networkID string) ([]*resources.NetworkResource, error) + DeleteResource(tx db.Transaction, resource *resources.NetworkResource) error +} + +type repository struct { + store *db.Store +} + +func newRepository(s *db.Store) Repository { + return &repository{store: s} +} + +func (r *repository) Store() *db.Store { + return r.store +} + +func (r *repository) GetResourcesByNetworkID(tx db.Transaction, strength db.LockingStrength, networkID string) ([]*resources.NetworkResource, error) { + var resources []*resources.NetworkResource + err := r.store.GetMany(tx, strength, &resources, "network_id = ?", networkID) + if err != nil { + return nil, err + } + return resources, nil +} + +func (r *repository) DeleteResource(tx db.Transaction, resource *resources.NetworkResource) error { + return r.store.Delete(tx, resource) +} diff --git a/internal/modules/networks/resources/types.go b/internal/modules/networks/resources/types.go new file mode 100644 index 0000000..2aaa378 --- /dev/null +++ b/internal/modules/networks/resources/types.go @@ -0,0 +1,173 @@ +package resources + +import ( + "errors" + "fmt" + "net/netip" + "regexp" + + "github.com/netbirdio/netbird/management/server/http/api" + nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/route" + "github.com/rs/xid" + + "management/internal/modules/networks" + "management/internal/modules/networks/routers" +) + +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:"primaryKey"` + 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"` + Enabled bool +} + +func NewNetworkResource(accountID, networkID, name, description, address string, groupIDs []string, enabled bool) (*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, + Enabled: enabled, + }, 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, + Enabled: n.Enabled, + } +} + +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 + n.Enabled = req.Enabled +} + +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, + Enabled: n.Enabled, + } +} + +func (n *NetworkResource) ToRoute(peer *nbpeer.Peer, router *routers.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, + PeerID: peer.ID, + PeerGroups: nil, + Masquerade: router.Masquerade, + Metric: router.Metric, + Enabled: n.Enabled, + 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 *networks.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") +} diff --git a/internal/modules/networks/routers/interface.go b/internal/modules/networks/routers/interface.go new file mode 100644 index 0000000..c8edf76 --- /dev/null +++ b/internal/modules/networks/routers/interface.go @@ -0,0 +1,13 @@ +package routers + +import ( + "context" + + "management/internal/shared/db" +) + +type Manager interface { + GetNetworkRoutersByNetID(ctx context.Context, tx db.Transaction, lockingStrength db.LockingStrength, accountID, userID, networkID string) ([]*NetworkRouter, error) + DeleteRouter(ctx context.Context, tx db.Transaction, accountID, userID, networkID, routerID string) error + DeleteRoutersInNetwork(ctx context.Context, tx db.Transaction, accountID, userID, networkID string) error +} diff --git a/internal/modules/networks/routers/manager/api.go b/internal/modules/networks/routers/manager/api.go new file mode 100644 index 0000000..5d04392 --- /dev/null +++ b/internal/modules/networks/routers/manager/api.go @@ -0,0 +1 @@ +package manager diff --git a/internal/modules/networks/routers/manager/manager.go b/internal/modules/networks/routers/manager/manager.go new file mode 100644 index 0000000..5d04392 --- /dev/null +++ b/internal/modules/networks/routers/manager/manager.go @@ -0,0 +1 @@ +package manager diff --git a/internal/modules/networks/routers/manager/metrics.go b/internal/modules/networks/routers/manager/metrics.go new file mode 100644 index 0000000..5d04392 --- /dev/null +++ b/internal/modules/networks/routers/manager/metrics.go @@ -0,0 +1 @@ +package manager diff --git a/internal/modules/networks/routers/manager/repository.go b/internal/modules/networks/routers/manager/repository.go new file mode 100644 index 0000000..5d04392 --- /dev/null +++ b/internal/modules/networks/routers/manager/repository.go @@ -0,0 +1 @@ +package manager diff --git a/internal/modules/networks/routers/types.go b/internal/modules/networks/routers/types.go new file mode 100644 index 0000000..da73742 --- /dev/null +++ b/internal/modules/networks/routers/types.go @@ -0,0 +1,80 @@ +package routers + +import ( + "errors" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/rs/xid" + + "management/internal/modules/networks" +) + +type NetworkRouter struct { + ID string `gorm:"primaryKey"` + NetworkID string `gorm:"index"` + AccountID string `gorm:"index"` + Peer string + PeerGroups []string `gorm:"serializer:json"` + Masquerade bool + Metric int + Enabled bool +} + +func NewNetworkRouter(accountID string, networkID string, peer string, peerGroups []string, masquerade bool, metric int, enabled bool) (*NetworkRouter, error) { + if peer != "" && len(peerGroups) > 0 { + return nil, errors.New("peer and peerGroups cannot be set at the same time") + } + + return &NetworkRouter{ + ID: xid.New().String(), + AccountID: accountID, + NetworkID: networkID, + Peer: peer, + PeerGroups: peerGroups, + Masquerade: masquerade, + Metric: metric, + Enabled: enabled, + }, nil +} + +func (n *NetworkRouter) ToAPIResponse() *api.NetworkRouter { + return &api.NetworkRouter{ + Id: n.ID, + Peer: &n.Peer, + PeerGroups: &n.PeerGroups, + Masquerade: n.Masquerade, + Metric: n.Metric, + Enabled: n.Enabled, + } +} + +func (n *NetworkRouter) FromAPIRequest(req *api.NetworkRouterRequest) { + if req.Peer != nil { + n.Peer = *req.Peer + } + + if req.PeerGroups != nil { + n.PeerGroups = *req.PeerGroups + } + + n.Masquerade = req.Masquerade + n.Metric = req.Metric + n.Enabled = req.Enabled +} + +func (n *NetworkRouter) Copy() *NetworkRouter { + return &NetworkRouter{ + ID: n.ID, + NetworkID: n.NetworkID, + AccountID: n.AccountID, + Peer: n.Peer, + PeerGroups: n.PeerGroups, + Masquerade: n.Masquerade, + Metric: n.Metric, + Enabled: n.Enabled, + } +} + +func (n *NetworkRouter) EventMeta(network *networks.Network) map[string]any { + return map[string]any{"network_name": network.Name, "network_id": network.ID, "peer": n.Peer, "peer_groups": n.PeerGroups} +} diff --git a/internal/modules/peers/manager.go b/internal/modules/peers/manager.go index be5af1d..6179904 100644 --- a/internal/modules/peers/manager.go +++ b/internal/modules/peers/manager.go @@ -6,6 +6,7 @@ import ( "github.com/gorilla/mux" "management/internal/modules/peers/types" + "management/internal/shared/activity" "management/internal/shared/db" "management/internal/shared/permissions" "management/pkg/logging" @@ -14,7 +15,8 @@ import ( var log = logging.LoggerForThisPackage() type Manager struct { - repo Repository + repo Repository + eventManager *activity.Manager } func NewManager(store *db.Store, router *mux.Router, permissionsManager permissions.Manager) *Manager { @@ -37,76 +39,21 @@ func (m *Manager) GetFilteredPeers(ctx context.Context, tx db.Transaction, stren return m.repo.GetFilteredPeers(tx, strength, accountID, nameFilter, ipFilter) } -import ( - "context" +func (m *Manager) UpdatePeer(ctx context.Context, tx db.Transaction, peer *types.Peer) error { + validateInput + validatePermissions + err := m.repo.RunInTx(func(tx db.Transaction) error { + othermanager.UpdatePeers + ourmanager.UpdateGroup + }) + if err != nil { + return err + } - "github.com/gorilla/mux" + err := sendPeerUpdateEvent(tx, peer) // -> goes to peerUpdtaeChannel + if err != nil { + log.Errorf("Failed to send peer update event: %v", err) + } - "management/internal/modules/peers/types" - "management/internal/shared/db" - "management/internal/shared/permissions" - "management/pkg/logging" -) - -var log = logging.LoggerForThisPackage() - -type Manager struct { - repo Repository -} - -func NewManager(store *db.Store, router *mux.Router, permissionsManager permissions.Manager) *Manager { - repo := newRepository(store) - m := &Manager{repo: repo} - api := newHandler(m, permissionsManager) - api.RegisterEndpoints(router) - return m -} - -func (m *Manager) GetPeer(ctx context.Context, tx db.Transaction, strength db.LockingStrength, accountID, peerID string) (*types.Peer, error) { - return m.repo.GetPeerByID(tx, strength, accountID, peerID) -} - -func (m *Manager) GetPeers(ctx context.Context, tx db.Transaction, strength db.LockingStrength, accountID string) ([]*types.Peer, error) { - return m.repo.GetPeers(tx, strength, accountID) -} - -func (m *Manager) GetFilteredPeers(ctx context.Context, tx db.Transaction, strength db.LockingStrength, accountID, nameFilter, ipFilter string) ([]*types.Peer, error) { - return m.repo.GetFilteredPeers(tx, strength, accountID, nameFilter, ipFilter) -} - -import ( - "context" - - "github.com/gorilla/mux" - - "management/internal/modules/peers/types" - "management/internal/shared/db" - "management/internal/shared/permissions" - "management/pkg/logging" -) - -var log = logging.LoggerForThisPackage() - -type Manager struct { - repo Repository -} - -func NewManager(store *db.Store, router *mux.Router, permissionsManager permissions.Manager) *Manager { - repo := newRepository(store) - m := &Manager{repo: repo} - api := newHandler(m, permissionsManager) - api.RegisterEndpoints(router) - return m -} - -func (m *Manager) GetPeer(ctx context.Context, tx db.Transaction, strength db.LockingStrength, accountID, peerID string) (*types.Peer, error) { - return m.repo.GetPeerByID(tx, strength, accountID, peerID) -} - -func (m *Manager) GetPeers(ctx context.Context, tx db.Transaction, strength db.LockingStrength, accountID string) ([]*types.Peer, error) { - return m.repo.GetPeers(tx, strength, accountID) -} - -func (m *Manager) GetFilteredPeers(ctx context.Context, tx db.Transaction, strength db.LockingStrength, accountID, nameFilter, ipFilter string) ([]*types.Peer, error) { - return m.repo.GetFilteredPeers(tx, strength, accountID, nameFilter, ipFilter) + return m.repo.UpdatePeer(tx, peer) } diff --git a/internal/modules/peers/repository.go b/internal/modules/peers/repository.go index 0c263b4..d333da3 100644 --- a/internal/modules/peers/repository.go +++ b/internal/modules/peers/repository.go @@ -10,6 +10,7 @@ type Repository interface { GetPeerByID(tx db.Transaction, strength db.LockingStrength, accountID, peerId string) (*types.Peer, error) GetPeers(tx db.Transaction, strength db.LockingStrength, accountID string) ([]*types.Peer, error) GetFilteredPeers(tx db.Transaction, strength db.LockingStrength, accountID string, nameFilter, ipFilter string) ([]*types.Peer, error) + UpdatePeer(tx db.Transaction, peer *types.Peer) error } type repository struct { @@ -63,3 +64,7 @@ func (r *repository) GetFilteredPeers(tx db.Transaction, strength db.LockingStre } return peers, nil } + +func (r *repository) UpdatePeer(tx db.Transaction, peer *types.Peer) error { + return r.store.Update(tx, peer) +} diff --git a/internal/modules/policies/manager/api.go b/internal/modules/policies/manager/api.go new file mode 100644 index 0000000..5d04392 --- /dev/null +++ b/internal/modules/policies/manager/api.go @@ -0,0 +1 @@ +package manager diff --git a/internal/modules/policies/manager/manager.go b/internal/modules/policies/manager/manager.go new file mode 100644 index 0000000..5d04392 --- /dev/null +++ b/internal/modules/policies/manager/manager.go @@ -0,0 +1 @@ +package manager diff --git a/internal/modules/policies/manager/repository.go b/internal/modules/policies/manager/repository.go new file mode 100644 index 0000000..5d04392 --- /dev/null +++ b/internal/modules/policies/manager/repository.go @@ -0,0 +1 @@ +package manager diff --git a/internal/modules/policies/policy.go b/internal/modules/policies/policy.go new file mode 100644 index 0000000..95a4e99 --- /dev/null +++ b/internal/modules/policies/policy.go @@ -0,0 +1,136 @@ +package policies + +const ( + // PolicyTrafficActionAccept indicates that the traffic is accepted + PolicyTrafficActionAccept = PolicyTrafficActionType("accept") + // PolicyTrafficActionDrop indicates that the traffic is dropped + PolicyTrafficActionDrop = PolicyTrafficActionType("drop") +) + +const ( + // PolicyRuleProtocolALL type of traffic + PolicyRuleProtocolALL = PolicyRuleProtocolType("all") + // PolicyRuleProtocolTCP type of traffic + PolicyRuleProtocolTCP = PolicyRuleProtocolType("tcp") + // PolicyRuleProtocolUDP type of traffic + PolicyRuleProtocolUDP = PolicyRuleProtocolType("udp") + // PolicyRuleProtocolICMP type of traffic + PolicyRuleProtocolICMP = PolicyRuleProtocolType("icmp") +) + +const ( + // PolicyRuleFlowDirect allows traffic from source to destination + PolicyRuleFlowDirect = PolicyRuleDirection("direct") + // PolicyRuleFlowBidirect allows traffic to both directions + PolicyRuleFlowBidirect = PolicyRuleDirection("bidirect") +) + +const ( + // DefaultRuleName is a name for the Default rule that is created for every account + 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" +) + +// PolicyUpdateOperation operation object with type and values to be applied +type PolicyUpdateOperation struct { + Type PolicyUpdateOperationType + Values []string +} + +// Policy of the Rego query +type Policy struct { + // ID of the policy' + ID string `gorm:"primaryKey"` + + // AccountID is a reference to Account that this object belongs + AccountID string `json:"-" gorm:"index"` + + // Name of the Policy + Name string + + // Description of the policy visible in the UI + Description string + + // Enabled status of the policy + Enabled bool + + // Rules of the policy + Rules []*PolicyRule `gorm:"foreignKey:PolicyID;references:id;constraint:OnDelete:CASCADE;"` + + // SourcePostureChecks are ID references to Posture checks for policy source groups + SourcePostureChecks []string `gorm:"serializer:json"` +} + +// Copy returns a copy of the policy. +func (p *Policy) Copy() *Policy { + c := &Policy{ + ID: p.ID, + AccountID: p.AccountID, + Name: p.Name, + Description: p.Description, + Enabled: p.Enabled, + Rules: make([]*PolicyRule, len(p.Rules)), + SourcePostureChecks: make([]string, len(p.SourcePostureChecks)), + } + for i, r := range p.Rules { + c.Rules[i] = r.Copy() + } + copy(c.SourcePostureChecks, p.SourcePostureChecks) + 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} +} + +// UpgradeAndFix different version of policies to latest version +func (p *Policy) UpgradeAndFix() { + for _, r := range p.Rules { + // start migrate from version v0.20.3 + if r.Protocol == "" { + r.Protocol = PolicyRuleProtocolALL + } + if r.Protocol == PolicyRuleProtocolALL && !r.Bidirectional { + r.Bidirectional = true + } + // -- v0.20.4 + } +} + +// RuleGroups returns a list of all groups referenced in the policy's rules, +// including sources and destinations. +func (p *Policy) RuleGroups() []string { + groups := make([]string, 0) + for _, rule := range p.Rules { + groups = append(groups, rule.Sources...) + groups = append(groups, rule.Destinations...) + } + + return groups +} + +// SourceGroups returns a slice of all unique source groups referenced in the policy's rules. +func (p *Policy) SourceGroups() []string { + if len(p.Rules) == 1 { + return p.Rules[0].Sources + } + groups := make(map[string]struct{}, len(p.Rules)) + for _, rule := range p.Rules { + for _, source := range rule.Sources { + groups[source] = struct{}{} + } + } + + groupIDs := make([]string, 0, len(groups)) + for groupID := range groups { + groupIDs = append(groupIDs, groupID) + } + + return groupIDs +} diff --git a/internal/modules/policies/policyrule.go b/internal/modules/policies/policyrule.go new file mode 100644 index 0000000..c3193e1 --- /dev/null +++ b/internal/modules/policies/policyrule.go @@ -0,0 +1,110 @@ +package policies + +import ( + "github.com/netbirdio/netbird/management/proto" + + "management/internal/modules/groups" +) + +// PolicyUpdateOperationType operation type +type PolicyUpdateOperationType int + +// PolicyTrafficActionType action type for the firewall +type PolicyTrafficActionType string + +// PolicyRuleProtocolType type of traffic +type PolicyRuleProtocolType string + +// PolicyRuleDirection direction of traffic +type PolicyRuleDirection string + +// RulePortRange represents a range of ports for a firewall rule. +type RulePortRange struct { + Start uint16 + End uint16 +} + +func (r *RulePortRange) ToProto() *proto.PortInfo { + return &proto.PortInfo{ + PortSelection: &proto.PortInfo_Range_{ + Range: &proto.PortInfo_Range{ + Start: uint32(r.Start), + End: uint32(r.End), + }, + }, + } +} + +func (r *RulePortRange) Equal(other *RulePortRange) bool { + return r.Start == other.Start && r.End == other.End +} + +// PolicyRule is the metadata of the policy +type PolicyRule struct { + // ID of the policy rule + ID string `gorm:"primaryKey"` + + // PolicyID is a reference to Policy that this object belongs + PolicyID string `json:"-" gorm:"index"` + + // 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 `gorm:"serializer:json"` + + // DestinationResource policy destination resource that the rule is applied to + DestinationResource groups.Resource `gorm:"serializer:json"` + + // Sources policy source groups + Sources []string `gorm:"serializer:json"` + + // SourceResource policy source resource that the rule is applied to + SourceResource groups.Resource `gorm:"serializer:json"` + + // Bidirectional define if the rule is applicable in both directions, sources, and destinations + Bidirectional bool + + // Protocol type of the traffic + Protocol PolicyRuleProtocolType + + // Ports or it ranges list + Ports []string `gorm:"serializer:json"` + + // PortRanges a list of port ranges. + PortRanges []RulePortRange `gorm:"serializer:json"` +} + +// Copy returns a copy of a policy rule +func (pm *PolicyRule) Copy() *PolicyRule { + rule := &PolicyRule{ + ID: pm.ID, + PolicyID: pm.PolicyID, + Name: pm.Name, + Description: pm.Description, + Enabled: pm.Enabled, + Action: pm.Action, + Destinations: make([]string, len(pm.Destinations)), + DestinationResource: pm.DestinationResource, + Sources: make([]string, len(pm.Sources)), + SourceResource: pm.SourceResource, + Bidirectional: pm.Bidirectional, + Protocol: pm.Protocol, + Ports: make([]string, len(pm.Ports)), + PortRanges: make([]RulePortRange, len(pm.PortRanges)), + } + copy(rule.Destinations, pm.Destinations) + copy(rule.Sources, pm.Sources) + copy(rule.Ports, pm.Ports) + copy(rule.PortRanges, pm.PortRanges) + return rule +} diff --git a/internal/modules/template/interface.go b/internal/modules/template/interface.go new file mode 100644 index 0000000..7261413 --- /dev/null +++ b/internal/modules/template/interface.go @@ -0,0 +1,8 @@ +//go:build ignore +// +build ignore + +package template + +type Manager interface { + // Add all exported methods that the manager should implement +} diff --git a/internal/modules/template/manager/api.go b/internal/modules/template/manager/api.go new file mode 100644 index 0000000..9c2f04f --- /dev/null +++ b/internal/modules/template/manager/api.go @@ -0,0 +1,20 @@ +//go:build ignore +// +build ignore + +package manager + +type handler struct { + manager template.Manager + permissionsManager permissions.Manager +} + +func newHandler(manager template.Manager, permissionsManager permissions.Manager) *handler { + return &handler{ + manager: manager, + permissionsManager: permissionsManager, + } +} + +func (h *handler) RegisterEndpoints(router *mux.Router) { + // Register the API endpoints for the module +} diff --git a/internal/modules/template/manager/manager.go b/internal/modules/template/manager/manager.go new file mode 100644 index 0000000..a6aeb1f --- /dev/null +++ b/internal/modules/template/manager/manager.go @@ -0,0 +1,28 @@ +//go:build ignore +// +build ignore + +package manager + +import ( + "github.com/gorilla/mux" + + "management/internal/modules/template" + "management/internal/shared/db" + appmetrics "management/internal/shared/metrics" + "management/internal/shared/permissions" +) + +type managerImpl struct { + repo Repository +} + +func NewManager(store *db.Store, router *mux.Router, metrics appmetrics.AppMetrics, permissionsManager permissions.Manager) template.Manager { + repo := newRepository(store) + m := &managerImpl{ + repo: repo, + } + + api := newHandler(m, permissionsManager) + api.RegisterEndpoints(router) + return m +} diff --git a/internal/modules/template/manager/metrics.go b/internal/modules/template/manager/metrics.go new file mode 100644 index 0000000..0bc2399 --- /dev/null +++ b/internal/modules/template/manager/metrics.go @@ -0,0 +1,27 @@ +//go:build ignore +// +build ignore + +package manager + +type metrics struct { + dbAccessDuration metric.Int64Histogram +} + +func newMetrics(meter metric.Meter) (*metrics, error) { + dbAccessDuration, err := meter.Int64Histogram( + "sync_request_duration_seconds", + metric.WithDescription("Duration of sync requests in seconds"), + metric.WithUnit("s"), + ) + if err != nil { + return nil, err + } + + return &metrics{ + dbAccessDuration: dbAccessDuration, + }, nil +} + +func (m *metrics) RecordDBAccessDuration(duration time.Duration) { + m.dbAccessDuration.Record(context.Background(), duration.Milliseconds(), metric.WithAttributes()) +} diff --git a/internal/modules/template/manager/repository.go b/internal/modules/template/manager/repository.go new file mode 100644 index 0000000..7495f8b --- /dev/null +++ b/internal/modules/template/manager/repository.go @@ -0,0 +1,4 @@ +//go:build ignore +// +build ignore + +package manager diff --git a/internal/modules/template/type.go b/internal/modules/template/type.go new file mode 100644 index 0000000..72f9c77 --- /dev/null +++ b/internal/modules/template/type.go @@ -0,0 +1,4 @@ +//go:build ignore +// +build ignore + +package template diff --git a/internal/modules/users/types/user.go b/internal/modules/users/types/user.go index 9abbffe..dcbd2f7 100644 --- a/internal/modules/users/types/user.go +++ b/internal/modules/users/types/user.go @@ -8,6 +8,7 @@ import ( "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/integration_reference" + "management/internal/modules/accounts/settings" "management/internal/modules/users/pats/types" ) @@ -129,7 +130,7 @@ func (u *User) IsRegularUser() bool { } // ToUserInfo converts a User object to a UserInfo object. -func (u *User) ToUserInfo(userData *idp.UserData, settings *Settings) (*UserInfo, error) { +func (u *User) ToUserInfo(userData *idp.UserData, settings *settings.Settings) (*UserInfo, error) { autoGroups := u.AutoGroups if autoGroups == nil { autoGroups = []string{} diff --git a/internal/server/boot.go b/internal/server/boot.go index de21449..2c958d5 100644 --- a/internal/server/boot.go +++ b/internal/server/boot.go @@ -6,10 +6,16 @@ import ( "context" "net/http" + "github.com/gorilla/mux" + + "management/internal/controllers/network_map" + "management/internal/modules/peers" "management/internal/shared/activity" "management/internal/shared/activity/sqlite" "management/internal/shared/api/rest" "management/internal/shared/db" + "management/internal/shared/metrics" + "management/internal/shared/permissions" ) func (s *Server) Store() *db.Store { @@ -26,7 +32,7 @@ func (s *Server) Store() *db.Store { func (s *Server) HttpServer() *http.Server { return Create(s, func() *http.Server { - router := rest.NewRouter() + router := s.Router() return &http.Server{ Addr: ":8080", // or from a config file @@ -35,6 +41,22 @@ func (s *Server) HttpServer() *http.Server { }) } +func (s *Server) Metrics() *metrics.AppMetrics { + return Create(s, func() *metrics.AppMetrics { + appMetrics, err := metrics.NewAppMetrics() + if err != nil { + log.Fatalf("error while creating app metrics: %s", err) + } + return appMetrics + }) +} + +func (s *Server) Router() *mux.Router { + return Create(s, func() *mux.Router { + return rest.NewRouter() + }) +} + func (s *Server) EventStore() activity.Store { return Create(s, func() activity.Store { ctx := context.Background() @@ -45,3 +67,27 @@ func (s *Server) EventStore() activity.Store { return store }) } + +func (s *Server) NetworkMapController() *network_map.Controller { + return Create(s, func() *network_map.Controller { + store := s.Store() + metrics := s.Metrics() + return network_map.NewController(store, metrics) + }) +} + +func (s *Server) PermissionsManager() permissions.Manager { + return Create(s, func() permissions.Manager { + return permissions.NewManager() + }) +} + +func (s *Server) PeersManager() *peers.Manager { + return Create(s, func() *peers.Manager { + store := s.Store() + router := s.Router() + permissionsManager := s.PermissionsManager() + + return peers.NewManager(store, router, permissionsManager) + }) +} diff --git a/internal/server/modules.go b/internal/server/modules.go index f62ffff..6b6a214 100644 --- a/internal/server/modules.go +++ b/internal/server/modules.go @@ -1,3 +1,20 @@ package server -// @note this file includes the domain modules, accounts, peers, networks, etc. +import ( + "management/internal/modules/networks" + "management/internal/modules/networks/manager" + "management/internal/modules/networks/resources" + resourcesManager "management/internal/modules/networks/resources/manager" +) + +func (s *Server) NetworksManager() networks.Manager { + return Create(s, func() networks.Manager { + return manager.NewManager(s.Store(), s.Router(), s.PermissionsManager()) + }) +} + +func (s *Server) ResourcesManager() resources.Manager { + return Create(s, func() resources.Manager { + return resourcesManager.NewManager(s.Store(), s.Router(), s.NetworksManager()) + }) +} diff --git a/internal/shared/api/rest/middleware/logging_middleware.go b/internal/shared/api/rest/middleware/logging_middleware.go index c870d7c..a5447b3 100644 --- a/internal/shared/api/rest/middleware/logging_middleware.go +++ b/internal/shared/api/rest/middleware/logging_middleware.go @@ -1 +1,12 @@ package middleware + +import "net/http" + +// loggingMiddleware is an example that logs each incoming request. +// Replace with your logger of choice. +func LoggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // log.Printf("[%s] %s", r.Method, r.URL.Path) + next.ServeHTTP(w, r) + }) +} diff --git a/internal/shared/api/rest/middleware/recovery_middleware.go b/internal/shared/api/rest/middleware/recovery_middleware.go new file mode 100644 index 0000000..7184e6d --- /dev/null +++ b/internal/shared/api/rest/middleware/recovery_middleware.go @@ -0,0 +1,15 @@ +package middleware + +import "net/http" + +// RecoveryMiddleware recovers from panics and returns a 500 Internal Server Error. +func RecoveryMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rec := recover(); rec != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + }() + next.ServeHTTP(w, r) + }) +} diff --git a/internal/shared/api/rest/router.go b/internal/shared/api/rest/router.go index 931328b..55b7d9d 100644 --- a/internal/shared/api/rest/router.go +++ b/internal/shared/api/rest/router.go @@ -1,9 +1,9 @@ package rest import ( - "net/http" - "github.com/gorilla/mux" + + "management/internal/shared/api/rest/middleware" ) // NewRouter creates and returns a mux.Router configured with default middleware @@ -11,33 +11,8 @@ import ( func NewRouter() *mux.Router { r := mux.NewRouter() - // Attach middlewares - r.Use(loggingMiddleware) - r.Use(recoveryMiddleware) - - // Example endpoint - // r.HandleFunc("/health", healthCheckHandler).Methods("GET") + r.Use(middleware.LoggingMiddleware) + r.Use(middleware.RecoveryMiddleware) return r } - -// loggingMiddleware is an example that logs each incoming request. -// Replace with your logger of choice. -func loggingMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // log.Printf("[%s] %s", r.Method, r.URL.Path) - next.ServeHTTP(w, r) - }) -} - -// recoveryMiddleware recovers from panics and returns a 500 Internal Server Error. -func recoveryMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if rec := recover(); rec != nil { - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - } - }() - next.ServeHTTP(w, r) - }) -} diff --git a/internal/shared/db/store.go b/internal/shared/db/store.go index 3f88d1e..ed553d5 100644 --- a/internal/shared/db/store.go +++ b/internal/shared/db/store.go @@ -81,16 +81,8 @@ func (s *Store) GetMany(tx Transaction, strength LockingStrength, dest interface return db.Find(dest).Error } -func (s *Store) Delete(value interface{}) error { - return s.db.Delete(value).Error -} - -func (s *Store) Update(tx Transaction, value interface{}) error { - return s.Using(tx).Save(value).Error -} - -func (s *Store) Update(tx Transaction, value interface{}) error { - return s.Using(tx).Save(value).Error +func (s *Store) Delete(tx Transaction, value interface{}) error { + return s.Using(tx).Delete(value).Error } func (s *Store) Update(tx Transaction, value interface{}) error { diff --git a/internal/shared/db/transaction.go b/internal/shared/db/transaction.go index 49708b3..3e5bad1 100644 --- a/internal/shared/db/transaction.go +++ b/internal/shared/db/transaction.go @@ -1,6 +1,8 @@ package db -import "gorm.io/gorm" +import ( + "gorm.io/gorm" +) type LockingStrength string @@ -15,16 +17,52 @@ const ( type Transaction interface { Commit() error Rollback() error + AddEvent(event func()) + FlushEvents() +} + +type TransactionalManager[T any] interface { + UsingTx(tx Transaction) T } type storeTx struct { - db *gorm.DB + db *gorm.DB + events []func() } -func (t *storeTx) Commit() error { - return t.db.Commit().Error +func (tx *storeTx) Commit() error { + for _, e := range tx.events { + e() + } + return tx.db.Commit().Error } -func (t *storeTx) Rollback() error { - return t.db.Rollback().Error +func (tx *storeTx) Rollback() error { + return tx.db.Rollback().Error +} + +func (tx *storeTx) AddEvent(fn func()) { + tx.events = append(tx.events, fn) +} + +func (tx *storeTx) FlushEvents() { + for _, e := range tx.events { + e() + } + tx.events = nil +} + +func WithTx(store *Store, parentTx Transaction, fn func(tx Transaction) error) error { + if parentTx != nil { + return fn(parentTx) + } + + return store.RunInTx(func(tx Transaction) error { + if err := fn(tx); err != nil { + return err + } + + tx.FlushEvents() + return nil + }) } diff --git a/internal/shared/metrics/metrics.go b/internal/shared/metrics/metrics.go new file mode 100644 index 0000000..9f7d99c --- /dev/null +++ b/internal/shared/metrics/metrics.go @@ -0,0 +1,83 @@ +package metrics + +import ( + "context" + "fmt" + "net" + "net/http" + "reflect" + + "github.com/gorilla/mux" + prometheus2 "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/exporters/prometheus" + metric2 "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk/metric" +) + +const defaultEndpoint = "/metrics" + +type AppMetrics struct { + meter metric2.Meter + listener net.Listener + ctx context.Context +} + +func NewAppMetrics() (*AppMetrics, error) { + exporter, err := prometheus.New() + if err != nil { + return nil, err + } + + provider := metric.NewMeterProvider(metric.WithReader(exporter)) + pkg := reflect.TypeOf(defaultEndpoint).PkgPath() + meter := provider.Meter(pkg) + + return &AppMetrics{ + meter: meter, + }, nil +} + +// Expose metrics on a given port and endpoint. If endpoint is empty a defaultEndpoint one will be used. +// Exposes metrics in the Prometheus format https://prometheus.io/ +func (appMetrics *AppMetrics) Expose(ctx context.Context, port int, endpoint string) error { + if endpoint == "" { + endpoint = defaultEndpoint + } + rootRouter := mux.NewRouter() + rootRouter.Handle(endpoint, promhttp.HandlerFor( + prometheus2.DefaultGatherer, + promhttp.HandlerOpts{EnableOpenMetrics: true})) + listener, err := net.Listen("tcp4", fmt.Sprintf(":%d", port)) + if err != nil { + return err + } + appMetrics.listener = listener + go func() { + err := http.Serve(listener, rootRouter) + if err != nil { + return + } + }() + + log.WithContext(ctx).Infof("enabled application metrics and exposing on http://%s", listener.Addr().String()) + + return nil +} + +// Close stop application metrics HTTP handler and closes listener. +func (appMetrics *AppMetrics) Close() error { + if appMetrics.listener == nil { + return nil + } + return appMetrics.listener.Close() +} + +// func (appMetrics *AppMetrics) RegisterMetrics(fn func(meter metric2.Meter) error) error { +// return fn(appMetrics.meter) +// } + +func RegisterMetrics[T any](app *AppMetrics, fn func(metric2.Meter) (T, error)) (T, error) { + return fn(app.meter) +} diff --git a/pkg/logging/init.go b/pkg/logging/init.go index 349d266..b5aaa21 100644 --- a/pkg/logging/init.go +++ b/pkg/logging/init.go @@ -3,7 +3,7 @@ package logging import ( "fmt" "io" - "log" + "log/syslog" "os" "path/filepath" "runtime" @@ -13,11 +13,14 @@ import ( "sync" "github.com/sirupsen/logrus" + lSyslog "github.com/sirupsen/logrus/hooks/syslog" "github.com/spf13/viper" "google.golang.org/grpc/grpclog" "gopkg.in/natefinch/lumberjack.v2" ) +const defaultLogSize = 5 + // global map of package paths to *logrus.Logger var ( mu sync.RWMutex @@ -162,7 +165,7 @@ func InitLog(logLevel string, logPath string) error { } logrus.SetOutput(io.Writer(lumberjackLogger)) } else if logPath == "syslog" { - AddSyslogHook() + addSyslogHook() } //nolint:gocritic @@ -210,3 +213,12 @@ func getLogMaxSize() int { } return defaultLogSize } + +func addSyslogHook() { + hook, err := lSyslog.NewSyslogHook("", "", syslog.LOG_INFO, "") + + if err != nil { + logrus.Errorf("Failed creating syslog hook: %s", err) + } + logrus.AddHook(hook) +}