mirror of
https://github.com/netbirdio/netbird.git
synced 2024-12-24 15:48:52 +01:00
3bfa26b13b
Converts rules to Rego policies and allow users to write raw policies to set up connectivity and firewall on the clients.
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
|
|
}
|