mirror of
https://github.com/netbirdio/netbird.git
synced 2024-12-24 23:59:25 +01:00
452 lines
10 KiB
Go
452 lines
10 KiB
Go
|
package server
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
_ "embed"
|
||
|
"fmt"
|
||
|
"html/template"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/netbirdio/netbird/management/server/activity"
|
||
|
"github.com/netbirdio/netbird/management/server/status"
|
||
|
|
||
|
"github.com/open-policy-agent/opa/rego"
|
||
|
log "github.com/sirupsen/logrus"
|
||
|
)
|
||
|
|
||
|
// PolicyUpdateOperationType operation type
|
||
|
type PolicyUpdateOperationType int
|
||
|
|
||
|
// PolicyTrafficActionType action type for the firewall
|
||
|
type PolicyTrafficActionType string
|
||
|
|
||
|
const (
|
||
|
// PolicyTrafficActionAccept indicates that the traffic is accepted
|
||
|
PolicyTrafficActionAccept = PolicyTrafficActionType("accept")
|
||
|
// PolicyTrafficActionDrop indicates that the traffic is dropped
|
||
|
PolicyTrafficActionDrop = PolicyTrafficActionType("drop")
|
||
|
)
|
||
|
|
||
|
// PolicyUpdateOperation operation object with type and values to be applied
|
||
|
type PolicyUpdateOperation struct {
|
||
|
Type PolicyUpdateOperationType
|
||
|
Values []string
|
||
|
}
|
||
|
|
||
|
//go:embed rego/default_policy_module.rego
|
||
|
var defaultPolicyModule string
|
||
|
|
||
|
//go:embed rego/default_policy.rego
|
||
|
var defaultPolicyText string
|
||
|
|
||
|
// defaultPolicyTemplate is a template for the default policy
|
||
|
var defaultPolicyTemplate = template.Must(template.New("policy").Parse(defaultPolicyText))
|
||
|
|
||
|
// PolicyRule is the metadata of the policy
|
||
|
type PolicyRule struct {
|
||
|
// ID of the policy rule
|
||
|
ID string
|
||
|
|
||
|
// Name of the rule visible in the UI
|
||
|
Name string
|
||
|
|
||
|
// Description of the rule visible in the UI
|
||
|
Description string
|
||
|
|
||
|
// Enabled status of rule in the system
|
||
|
Enabled bool
|
||
|
|
||
|
// Action policy accept or drops packets
|
||
|
Action PolicyTrafficActionType
|
||
|
|
||
|
// Destinations policy destination groups
|
||
|
Destinations []string
|
||
|
|
||
|
// Sources policy source groups
|
||
|
Sources []string
|
||
|
}
|
||
|
|
||
|
// Copy returns a copy of a policy rule
|
||
|
func (pm *PolicyRule) Copy() *PolicyRule {
|
||
|
return &PolicyRule{
|
||
|
ID: pm.ID,
|
||
|
Name: pm.Name,
|
||
|
Description: pm.Description,
|
||
|
Enabled: pm.Enabled,
|
||
|
Action: pm.Action,
|
||
|
Destinations: pm.Destinations[:],
|
||
|
Sources: pm.Sources[:],
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ToRule converts the PolicyRule to a legacy representation of the Rule (for backwards compatibility)
|
||
|
func (pm *PolicyRule) ToRule() *Rule {
|
||
|
return &Rule{
|
||
|
ID: pm.ID,
|
||
|
Name: pm.Name,
|
||
|
Description: pm.Description,
|
||
|
Disabled: !pm.Enabled,
|
||
|
Flow: TrafficFlowBidirect,
|
||
|
Destination: pm.Destinations,
|
||
|
Source: pm.Sources,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Policy of the Rego query
|
||
|
type Policy struct {
|
||
|
// ID of the policy
|
||
|
ID string
|
||
|
|
||
|
// Name of the Policy
|
||
|
Name string
|
||
|
|
||
|
// Description of the policy visible in the UI
|
||
|
Description string
|
||
|
|
||
|
// Enabled status of the policy
|
||
|
Enabled bool
|
||
|
|
||
|
// Query of Rego the policy
|
||
|
Query string
|
||
|
|
||
|
// Rules of the policy
|
||
|
Rules []*PolicyRule
|
||
|
}
|
||
|
|
||
|
// Copy returns a copy of the policy.
|
||
|
func (p *Policy) Copy() *Policy {
|
||
|
c := &Policy{
|
||
|
ID: p.ID,
|
||
|
Name: p.Name,
|
||
|
Description: p.Description,
|
||
|
Enabled: p.Enabled,
|
||
|
Query: p.Query,
|
||
|
}
|
||
|
for _, r := range p.Rules {
|
||
|
c.Rules = append(c.Rules, r.Copy())
|
||
|
}
|
||
|
return c
|
||
|
}
|
||
|
|
||
|
// EventMeta returns activity event meta related to this policy
|
||
|
func (p *Policy) EventMeta() map[string]any {
|
||
|
return map[string]any{"name": p.Name}
|
||
|
}
|
||
|
|
||
|
// UpdateQueryFromRules marshals policy rules to Rego string and set it to Query
|
||
|
func (p *Policy) UpdateQueryFromRules() error {
|
||
|
type templateVars struct {
|
||
|
All []string
|
||
|
Source []string
|
||
|
Destination []string
|
||
|
}
|
||
|
queries := []string{}
|
||
|
for _, r := range p.Rules {
|
||
|
if !r.Enabled {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
buff := new(bytes.Buffer)
|
||
|
input := templateVars{
|
||
|
All: append(r.Destinations[:], r.Sources...),
|
||
|
Source: r.Sources,
|
||
|
Destination: r.Destinations,
|
||
|
}
|
||
|
if err := defaultPolicyTemplate.Execute(buff, input); err != nil {
|
||
|
return status.Errorf(status.BadRequest, "failed to update policy query: %v", err)
|
||
|
}
|
||
|
queries = append(queries, buff.String())
|
||
|
}
|
||
|
p.Query = strings.Join(queries, "\n")
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// FirewallRule is a rule of the firewall.
|
||
|
type FirewallRule struct {
|
||
|
// PeerID of the peer
|
||
|
PeerID string
|
||
|
|
||
|
// PeerIP of the peer
|
||
|
PeerIP string
|
||
|
|
||
|
// Direction of the traffic
|
||
|
Direction string
|
||
|
|
||
|
// Action of the traffic
|
||
|
Action string
|
||
|
|
||
|
// Port of the traffic
|
||
|
Port string
|
||
|
}
|
||
|
|
||
|
// parseFromRegoResult parses the Rego result to a FirewallRule.
|
||
|
func (f *FirewallRule) parseFromRegoResult(value interface{}) error {
|
||
|
object, ok := value.(map[string]interface{})
|
||
|
if !ok {
|
||
|
return fmt.Errorf("invalid Rego query eval result")
|
||
|
}
|
||
|
|
||
|
peerID, ok := object["ID"].(string)
|
||
|
if !ok {
|
||
|
return fmt.Errorf("invalid Rego query eval result peer ID type")
|
||
|
}
|
||
|
|
||
|
peerIP, ok := object["IP"].(string)
|
||
|
if !ok {
|
||
|
return fmt.Errorf("invalid Rego query eval result peer IP type")
|
||
|
}
|
||
|
|
||
|
direction, ok := object["Direction"].(string)
|
||
|
if !ok {
|
||
|
return fmt.Errorf("invalid Rego query eval result peer direction type")
|
||
|
}
|
||
|
|
||
|
action, ok := object["Action"].(string)
|
||
|
if !ok {
|
||
|
return fmt.Errorf("invalid Rego query eval result peer action type")
|
||
|
}
|
||
|
|
||
|
port, ok := object["Port"].(string)
|
||
|
if !ok {
|
||
|
return fmt.Errorf("invalid Rego query eval result peer port type")
|
||
|
}
|
||
|
|
||
|
f.PeerID = peerID
|
||
|
f.PeerIP = peerIP
|
||
|
f.Direction = direction
|
||
|
f.Action = action
|
||
|
f.Port = port
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// getRegoQuery returns a initialized Rego object with default rule.
|
||
|
func (a *Account) getRegoQuery() (rego.PreparedEvalQuery, error) {
|
||
|
queries := []func(*rego.Rego){
|
||
|
rego.Query("data.netbird.all"),
|
||
|
rego.Module("netbird", defaultPolicyModule),
|
||
|
}
|
||
|
for i, p := range a.Policies {
|
||
|
if !p.Enabled {
|
||
|
continue
|
||
|
}
|
||
|
queries = append(queries, rego.Module(fmt.Sprintf("netbird-%d", i), p.Query))
|
||
|
}
|
||
|
return rego.New(queries...).PrepareForEval(context.TODO())
|
||
|
}
|
||
|
|
||
|
// getPeersByPolicy returns all peers that given peer has access to.
|
||
|
func (a *Account) getPeersByPolicy(peerID string) ([]*Peer, []*FirewallRule) {
|
||
|
input := map[string]interface{}{
|
||
|
"peer_id": peerID,
|
||
|
"peers": a.Peers,
|
||
|
"groups": a.Groups,
|
||
|
}
|
||
|
|
||
|
query, err := a.getRegoQuery()
|
||
|
if err != nil {
|
||
|
log.WithError(err).Error("get Rego query")
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
evalResult, err := query.Eval(
|
||
|
context.TODO(),
|
||
|
rego.EvalInput(input),
|
||
|
)
|
||
|
if err != nil {
|
||
|
log.WithError(err).Error("eval Rego query")
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
if len(evalResult) == 0 || len(evalResult[0].Expressions) == 0 {
|
||
|
log.Trace("empty Rego query eval result")
|
||
|
return nil, nil
|
||
|
}
|
||
|
expressions, ok := evalResult[0].Expressions[0].Value.([]interface{})
|
||
|
if !ok {
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
dst := make(map[string]struct{})
|
||
|
src := make(map[string]struct{})
|
||
|
peers := make([]*Peer, 0, len(expressions))
|
||
|
rules := make([]*FirewallRule, 0, len(expressions))
|
||
|
for _, v := range expressions {
|
||
|
rule := &FirewallRule{}
|
||
|
if err := rule.parseFromRegoResult(v); err != nil {
|
||
|
log.WithError(err).Error("parse Rego query eval result")
|
||
|
continue
|
||
|
}
|
||
|
rules = append(rules, rule)
|
||
|
switch rule.Direction {
|
||
|
case "dst":
|
||
|
if _, ok := dst[rule.PeerID]; ok {
|
||
|
continue
|
||
|
}
|
||
|
dst[rule.PeerID] = struct{}{}
|
||
|
case "src":
|
||
|
if _, ok := src[rule.PeerID]; ok {
|
||
|
continue
|
||
|
}
|
||
|
src[rule.PeerID] = struct{}{}
|
||
|
default:
|
||
|
log.WithField("direction", rule.Direction).Error("invalid direction")
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
added := make(map[string]struct{})
|
||
|
if _, ok := src[peerID]; ok {
|
||
|
for id := range dst {
|
||
|
if _, ok := added[id]; !ok && id != peerID {
|
||
|
added[id] = struct{}{}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if _, ok := dst[peerID]; ok {
|
||
|
for id := range src {
|
||
|
if _, ok := added[id]; !ok && id != peerID {
|
||
|
added[id] = struct{}{}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for id := range added {
|
||
|
peers = append(peers, a.Peers[id])
|
||
|
}
|
||
|
return peers, rules
|
||
|
}
|
||
|
|
||
|
// GetPolicy from the store
|
||
|
func (am *DefaultAccountManager) GetPolicy(accountID, policyID, userID string) (*Policy, error) {
|
||
|
unlock := am.Store.AcquireAccountLock(accountID)
|
||
|
defer unlock()
|
||
|
|
||
|
account, err := am.Store.GetAccount(accountID)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
user, err := account.FindUser(userID)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if !user.IsAdmin() {
|
||
|
return nil, status.Errorf(status.PermissionDenied, "only admins are allowed to view policies")
|
||
|
}
|
||
|
|
||
|
for _, policy := range account.Policies {
|
||
|
if policy.ID == policyID {
|
||
|
return policy, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil, status.Errorf(status.NotFound, "policy with ID %s not found", policyID)
|
||
|
}
|
||
|
|
||
|
// SavePolicy in the store
|
||
|
func (am *DefaultAccountManager) SavePolicy(accountID, userID string, policy *Policy) error {
|
||
|
unlock := am.Store.AcquireAccountLock(accountID)
|
||
|
defer unlock()
|
||
|
|
||
|
account, err := am.Store.GetAccount(accountID)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
exists := am.savePolicy(account, policy)
|
||
|
|
||
|
account.Network.IncSerial()
|
||
|
if err = am.Store.SaveAccount(account); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
action := activity.PolicyAdded
|
||
|
if exists {
|
||
|
action = activity.PolicyUpdated
|
||
|
}
|
||
|
am.storeEvent(userID, policy.ID, accountID, action, policy.EventMeta())
|
||
|
|
||
|
return am.updateAccountPeers(account)
|
||
|
}
|
||
|
|
||
|
// DeletePolicy from the store
|
||
|
func (am *DefaultAccountManager) DeletePolicy(accountID, policyID, userID string) error {
|
||
|
unlock := am.Store.AcquireAccountLock(accountID)
|
||
|
defer unlock()
|
||
|
|
||
|
account, err := am.Store.GetAccount(accountID)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
policy, err := am.deletePolicy(account, policyID)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
account.Network.IncSerial()
|
||
|
if err = am.Store.SaveAccount(account); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
am.storeEvent(userID, policy.ID, accountID, activity.PolicyRemoved, policy.EventMeta())
|
||
|
|
||
|
return am.updateAccountPeers(account)
|
||
|
}
|
||
|
|
||
|
// ListPolicies from the store
|
||
|
func (am *DefaultAccountManager) ListPolicies(accountID, userID string) ([]*Policy, error) {
|
||
|
unlock := am.Store.AcquireAccountLock(accountID)
|
||
|
defer unlock()
|
||
|
|
||
|
account, err := am.Store.GetAccount(accountID)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
user, err := account.FindUser(userID)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if !user.IsAdmin() {
|
||
|
return nil, status.Errorf(status.PermissionDenied, "Only Administrators can view policies")
|
||
|
}
|
||
|
|
||
|
return account.Policies[:], nil
|
||
|
}
|
||
|
|
||
|
func (am *DefaultAccountManager) deletePolicy(account *Account, policyID string) (*Policy, error) {
|
||
|
policyIdx := -1
|
||
|
for i, policy := range account.Policies {
|
||
|
if policy.ID == policyID {
|
||
|
policyIdx = i
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if policyIdx < 0 {
|
||
|
return nil, status.Errorf(status.NotFound, "rule with ID %s doesn't exist", policyID)
|
||
|
}
|
||
|
|
||
|
policy := account.Policies[policyIdx]
|
||
|
account.Policies = append(account.Policies[:policyIdx], account.Policies[policyIdx+1:]...)
|
||
|
return policy, nil
|
||
|
}
|
||
|
|
||
|
func (am *DefaultAccountManager) savePolicy(account *Account, policy *Policy) (exists bool) {
|
||
|
for i, p := range account.Policies {
|
||
|
if p.ID == policy.ID {
|
||
|
account.Policies[i] = policy
|
||
|
exists = true
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if !exists {
|
||
|
account.Policies = append(account.Policies, policy)
|
||
|
}
|
||
|
return
|
||
|
}
|