From 5dc0ff42a5fe01592b4bcdbd7aeb0eafb3713f25 Mon Sep 17 00:00:00 2001 From: Givi Khojanashvili Date: Sat, 1 Apr 2023 14:02:08 +0400 Subject: [PATCH] Fix broken auto-generated Rego rule (#769) Default Rego policy generated from the rules in some cases is broken. This change fixes the Rego template for rules to generate policies. Also, file store load constantly regenerates policy objects from rules. It allows updating/fixing of the default Rego template during releases. --- go.mod | 2 +- management/server/file_store.go | 26 +- management/server/policy.go | 64 ++-- management/server/policy_test.go | 281 +++++++++++++++--- management/server/rego/default_policy.rego | 10 +- .../server/rego/default_policy_module.rego | 12 +- 6 files changed, 315 insertions(+), 80 deletions(-) diff --git a/go.mod b/go.mod index c74fee409..5220eab6e 100644 --- a/go.mod +++ b/go.mod @@ -56,6 +56,7 @@ require ( go.opentelemetry.io/otel/exporters/prometheus v0.33.0 go.opentelemetry.io/otel/metric v0.33.0 go.opentelemetry.io/otel/sdk/metric v0.33.0 + golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf golang.org/x/net v0.8.0 golang.org/x/sync v0.1.0 golang.org/x/term v0.6.0 @@ -126,7 +127,6 @@ require ( go.opentelemetry.io/otel v1.11.1 // indirect go.opentelemetry.io/otel/sdk v1.11.1 // indirect go.opentelemetry.io/otel/trace v1.11.1 // indirect - golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf // indirect golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // indirect golang.org/x/mod v0.8.0 // indirect golang.org/x/text v0.8.0 // indirect diff --git a/management/server/file_store.go b/management/server/file_store.go index 9b4a9f47f..f79179841 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -121,15 +121,25 @@ func restore(file string) (*FileStore, error) { store.PrivateDomain2AccountID[account.Domain] = accountID } - // if no policies are defined, that means we need to migrate Rules to policies - if len(account.Policies) == 0 { + // TODO: policy query generated from the Go template and rule object. + // We need to refactor this part to avoid using templating for policies queries building + // and drop this migration part. + policies := make(map[string]int, len(account.Policies)) + for i, policy := range account.Policies { + policies[policy.ID] = i + } + if account.Policies == nil { account.Policies = make([]*Policy, 0) - for _, rule := range account.Rules { - policy, err := RuleToPolicy(rule) - if err != nil { - log.Errorf("unable to migrate rule to policy: %v", err) - continue - } + } + for _, rule := range account.Rules { + policy, err := RuleToPolicy(rule) + if err != nil { + log.Errorf("unable to migrate rule to policy: %v", err) + continue + } + if i, ok := policies[policy.ID]; ok { + account.Policies[i] = policy + } else { account.Policies = append(account.Policies, policy) } } diff --git a/management/server/policy.go b/management/server/policy.go index 31f6bb655..8a166c25c 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -178,6 +178,9 @@ type FirewallRule struct { // Port of the traffic Port string + + // id for internal purposes + id string } // parseFromRegoResult parses the Rego result to a FirewallRule. @@ -218,39 +221,35 @@ func (f *FirewallRule) parseFromRegoResult(value interface{}) error { f.Action = action f.Port = port + // NOTE: update this id each time when new field added + f.id = peerID + peerIP + direction + action + 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) { +// queryPeersAndFwRulesByRego returns a list associated Peers and firewall rules list for this peer. +func (a *Account) queryPeersAndFwRulesByRego( + peerID string, + queryNumber int, + query string, +) ([]*Peer, []*FirewallRule) { input := map[string]interface{}{ "peer_id": peerID, "peers": a.Peers, "groups": a.Groups, } - query, err := a.getRegoQuery() + stmt, err := rego.New( + rego.Query("data.netbird.all"), + rego.Module("netbird", defaultPolicyModule), + rego.Module(fmt.Sprintf("netbird-%d", queryNumber), query), + ).PrepareForEval(context.TODO()) if err != nil { log.WithError(err).Error("get Rego query") return nil, nil } - evalResult, err := query.Eval( + evalResult, err := stmt.Eval( context.TODO(), rego.EvalInput(input), ) @@ -318,6 +317,33 @@ func (a *Account) getPeersByPolicy(peerID string) ([]*Peer, []*FirewallRule) { return peers, rules } +// getPeersByPolicy returns all peers that given peer has access to. +func (a *Account) getPeersByPolicy(peerID string) (peers []*Peer, rules []*FirewallRule) { + peersSeen := make(map[string]struct{}) + ruleSeen := make(map[string]struct{}) + for i, policy := range a.Policies { + if !policy.Enabled { + continue + } + p, r := a.queryPeersAndFwRulesByRego(peerID, i, policy.Query) + for _, peer := range p { + if _, ok := peersSeen[peer.ID]; ok { + continue + } + peers = append(peers, peer) + peersSeen[peer.ID] = struct{}{} + } + for _, rule := range r { + if _, ok := ruleSeen[rule.id]; ok { + continue + } + rules = append(rules, rule) + ruleSeen[rule.id] = struct{}{} + } + } + return +} + // GetPolicy from the store func (am *DefaultAccountManager) GetPolicy(accountID, policyID, userID string) (*Policy, error) { unlock := am.Store.AcquireAccountLock(accountID) diff --git a/management/server/policy_test.go b/management/server/policy_test.go index 73663a8fd..39ac44843 100644 --- a/management/server/policy_test.go +++ b/management/server/policy_test.go @@ -5,63 +5,268 @@ import ( "testing" "github.com/stretchr/testify/assert" + "golang.org/x/exp/slices" ) func TestAccount_getPeersByPolicy(t *testing.T) { account := &Account{ Peers: map[string]*Peer{ - "peer1": { - ID: "peer1", - IP: net.IPv4(10, 20, 0, 1), + "cfif97at2r9s73au3q00": { + ID: "cfif97at2r9s73au3q00", + IP: net.ParseIP("100.65.14.88"), }, - "peer2": { - ID: "peer2", - IP: net.IPv4(10, 20, 0, 2), + "cfif97at2r9s73au3q0g": { + ID: "cfif97at2r9s73au3q0g", + IP: net.ParseIP("100.65.80.39"), }, - "peer3": { - ID: "peer3", - IP: net.IPv4(10, 20, 0, 3), + "cfif97at2r9s73au3q10": { + ID: "cfif97at2r9s73au3q10", + IP: net.ParseIP("100.65.254.139"), + }, + "cfif97at2r9s73au3q20": { + ID: "cfif97at2r9s73au3q20", + IP: net.ParseIP("100.65.62.5"), + }, + "cfj4tiqt2r9s73dmeun0": { + ID: "cfj4tiqt2r9s73dmeun0", + IP: net.ParseIP("100.65.32.206"), + }, + "cg7h032t2r9s73cg5fk0": { + ID: "cg7h032t2r9s73cg5fk0", + IP: net.ParseIP("100.65.250.202"), + }, + "cgcnkj2t2r9s73cg5vv0": { + ID: "cgcnkj2t2r9s73cg5vv0", + IP: net.ParseIP("100.65.13.186"), + }, + "cgcol4qt2r9s73cg601g": { + ID: "cgcol4qt2r9s73cg601g", + IP: net.ParseIP("100.65.29.55"), }, }, Groups: map[string]*Group{ - "gid1": { - ID: "gid1", - Name: "all", - Peers: []string{"peer1", "peer2", "peer3"}, + "cet9e92t2r9s7383ns20": { + ID: "cet9e92t2r9s7383ns20", + Name: "All", + Peers: []string{ + "cfif97at2r9s73au3q0g", + "cfif97at2r9s73au3q00", + "cfif97at2r9s73au3q20", + "cfif97at2r9s73au3q10", + "cfj4tiqt2r9s73dmeun0", + "cg7h032t2r9s73cg5fk0", + "cgcnkj2t2r9s73cg5vv0", + "cgcol4qt2r9s73cg601g", + }, + }, + "cev90bat2r9s7383o150": { + ID: "cev90bat2r9s7383o150", + Name: "swarm", + Peers: []string{ + "cfif97at2r9s73au3q0g", + "cfif97at2r9s73au3q00", + "cfif97at2r9s73au3q20", + "cfj4tiqt2r9s73dmeun0", + "cgcnkj2t2r9s73cg5vv0", + "cgcol4qt2r9s73cg601g", + }, }, }, Rules: map[string]*Rule{ - "default": { - ID: "default", - Name: "default", - Description: "default", - Disabled: false, - Source: []string{"gid1"}, - Destination: []string{"gid1"}, + "cet9e92t2r9s7383ns2g": { + ID: "cet9e92t2r9s7383ns2g", + Name: "Default", + Description: "This is a default rule that allows connections between all the resources", + Source: []string{ + "cet9e92t2r9s7383ns20", + }, + Destination: []string{ + "cet9e92t2r9s7383ns20", + }, + }, + "cev90bat2r9s7383o15g": { + ID: "cev90bat2r9s7383o15g", + Name: "Swarm", + Description: "", + Source: []string{ + "cev90bat2r9s7383o150", + "cet9e92t2r9s7383ns20", + }, + Destination: []string{ + "cev90bat2r9s7383o150", + }, }, }, } - rule, err := RuleToPolicy(account.Rules["default"]) + rule1, err := RuleToPolicy(account.Rules["cet9e92t2r9s7383ns2g"]) assert.NoError(t, err) - account.Policies = append(account.Policies, rule) + rule2, err := RuleToPolicy(account.Rules["cev90bat2r9s7383o15g"]) + assert.NoError(t, err) - peers, firewallRules := account.getPeersByPolicy("peer1") - assert.Len(t, peers, 2) - assert.Contains(t, peers, account.Peers["peer2"]) - assert.Contains(t, peers, account.Peers["peer3"]) + account.Policies = append(account.Policies, rule1, rule2) - epectedFirewallRules := []*FirewallRule{ - {PeerID: "peer1", PeerIP: "10.20.0.1", Direction: "dst", Action: "accept", Port: ""}, - {PeerID: "peer2", PeerIP: "10.20.0.2", Direction: "dst", Action: "accept", Port: ""}, - {PeerID: "peer3", PeerIP: "10.20.0.3", Direction: "dst", Action: "accept", Port: ""}, - {PeerID: "peer1", PeerIP: "10.20.0.1", Direction: "src", Action: "accept", Port: ""}, - {PeerID: "peer2", PeerIP: "10.20.0.2", Direction: "src", Action: "accept", Port: ""}, - {PeerID: "peer3", PeerIP: "10.20.0.3", Direction: "src", Action: "accept", Port: ""}, - } - assert.Len(t, firewallRules, len(epectedFirewallRules)) - for i := range firewallRules { - assert.Equal(t, firewallRules[i], epectedFirewallRules[i]) - } + t.Run("check that all peers get map", func(t *testing.T) { + for _, p := range account.Peers { + peers, firewallRules := account.getPeersByPolicy(p.ID) + assert.GreaterOrEqual(t, len(peers), 2, "mininum number peers should present") + assert.GreaterOrEqual(t, len(firewallRules), 2, "mininum number of firewall rules should present") + } + }) + + t.Run("check first peer map details", func(t *testing.T) { + peers, firewallRules := account.getPeersByPolicy("cfif97at2r9s73au3q0g") + assert.Len(t, peers, 7) + assert.Contains(t, peers, account.Peers["cfif97at2r9s73au3q00"]) + assert.Contains(t, peers, account.Peers["cfif97at2r9s73au3q10"]) + assert.Contains(t, peers, account.Peers["cfif97at2r9s73au3q20"]) + assert.Contains(t, peers, account.Peers["cfj4tiqt2r9s73dmeun0"]) + assert.Contains(t, peers, account.Peers["cg7h032t2r9s73cg5fk0"]) + + epectedFirewallRules := []*FirewallRule{ + { + PeerID: "cfif97at2r9s73au3q00", + PeerIP: "100.65.14.88", + Direction: "src", + Action: "accept", + Port: "", + id: "cfif97at2r9s73au3q00100.65.14.88srcaccept", + }, + { + PeerID: "cfif97at2r9s73au3q00", + PeerIP: "100.65.14.88", + Direction: "dst", + Action: "accept", + Port: "", + id: "cfif97at2r9s73au3q00100.65.14.88dstaccept", + }, + + { + PeerID: "cfif97at2r9s73au3q0g", + PeerIP: "100.65.80.39", + Direction: "dst", + Action: "accept", + Port: "", + id: "cfif97at2r9s73au3q0g100.65.80.39dstaccept", + }, + { + PeerID: "cfif97at2r9s73au3q0g", + PeerIP: "100.65.80.39", + Direction: "src", + Action: "accept", + Port: "", + id: "cfif97at2r9s73au3q0g100.65.80.39srcaccept", + }, + + { + PeerID: "cfif97at2r9s73au3q10", + PeerIP: "100.65.254.139", + Direction: "dst", + Action: "accept", + Port: "", + id: "cfif97at2r9s73au3q10100.65.254.139dstaccept", + }, + { + PeerID: "cfif97at2r9s73au3q10", + PeerIP: "100.65.254.139", + Direction: "src", + Action: "accept", + Port: "", + id: "cfif97at2r9s73au3q10100.65.254.139srcaccept", + }, + + { + PeerID: "cfif97at2r9s73au3q20", + PeerIP: "100.65.62.5", + Direction: "dst", + Action: "accept", + Port: "", + id: "cfif97at2r9s73au3q20100.65.62.5dstaccept", + }, + { + PeerID: "cfif97at2r9s73au3q20", + PeerIP: "100.65.62.5", + Direction: "src", + Action: "accept", + Port: "", + id: "cfif97at2r9s73au3q20100.65.62.5srcaccept", + }, + + { + PeerID: "cfj4tiqt2r9s73dmeun0", + PeerIP: "100.65.32.206", + Direction: "dst", + Action: "accept", + Port: "", + id: "cfj4tiqt2r9s73dmeun0100.65.32.206dstaccept", + }, + { + PeerID: "cfj4tiqt2r9s73dmeun0", + PeerIP: "100.65.32.206", + Direction: "src", + Action: "accept", + Port: "", + id: "cfj4tiqt2r9s73dmeun0100.65.32.206srcaccept", + }, + + { + PeerID: "cg7h032t2r9s73cg5fk0", + PeerIP: "100.65.250.202", + Direction: "dst", + Action: "accept", + Port: "", + id: "cg7h032t2r9s73cg5fk0100.65.250.202dstaccept", + }, + { + PeerID: "cg7h032t2r9s73cg5fk0", + PeerIP: "100.65.250.202", + Direction: "src", + Action: "accept", + Port: "", + id: "cg7h032t2r9s73cg5fk0100.65.250.202srcaccept", + }, + + { + PeerID: "cgcnkj2t2r9s73cg5vv0", + PeerIP: "100.65.13.186", + Direction: "dst", + Action: "accept", + Port: "", + id: "cgcnkj2t2r9s73cg5vv0100.65.13.186dstaccept", + }, + { + PeerID: "cgcnkj2t2r9s73cg5vv0", + PeerIP: "100.65.13.186", + Direction: "src", + Action: "accept", + Port: "", + id: "cgcnkj2t2r9s73cg5vv0100.65.13.186srcaccept", + }, + + { + PeerID: "cgcol4qt2r9s73cg601g", + PeerIP: "100.65.29.55", + Direction: "dst", + Action: "accept", + Port: "", + id: "cgcol4qt2r9s73cg601g100.65.29.55dstaccept", + }, + { + PeerID: "cgcol4qt2r9s73cg601g", + PeerIP: "100.65.29.55", + Direction: "src", + Action: "accept", + Port: "", + id: "cgcol4qt2r9s73cg601g100.65.29.55srcaccept", + }, + } + assert.Len(t, firewallRules, len(epectedFirewallRules)) + slices.SortFunc(firewallRules, func(a, b *FirewallRule) bool { + return a.PeerID < b.PeerID + }) + for i := range firewallRules { + assert.Equal(t, epectedFirewallRules[i], firewallRules[i]) + } + }) } diff --git a/management/server/rego/default_policy.rego b/management/server/rego/default_policy.rego index 92e975a02..a1012ae76 100644 --- a/management/server/rego/default_policy.rego +++ b/management/server/rego/default_policy.rego @@ -1,9 +1,9 @@ package netbird all[rule] { - is_peer_in_any_group([{{range $i, $e := .All}}{{if $i}},{{end}}"{{$e}}"{{end}}]) - rule := array.concat( - rules_from_groups([{{range $i, $e := .Destination}}{{if $i}},{{end}}"{{$e}}"{{end}}], "dst", "accept", ""), - rules_from_groups([{{range $i, $e := .Source}}{{if $i}},{{end}}"{{$e}}"{{end}}], "src", "accept", ""), - )[_] + is_peer_in_any_group([{{range $i, $e := .All}}{{if $i}},{{end}}"{{$e}}"{{end}}]) + rule := { + {{range $i, $e := .Destination}}rules_from_group("{{$e}}", "dst", "accept", ""),{{end}} + {{range $i, $e := .Source}}rules_from_group("{{$e}}", "src", "accept", ""),{{end}} + }[_][_] } diff --git a/management/server/rego/default_policy_module.rego b/management/server/rego/default_policy_module.rego index 846e22e21..7411db36a 100644 --- a/management/server/rego/default_policy_module.rego +++ b/management/server/rego/default_policy_module.rego @@ -17,17 +17,11 @@ get_rule(peer_id, direction, action, port) := rule if { } } -# peers_from_group returns a list of peer ids for a given group id -peers_from_group(group_id) := peers if { +# netbird_rules_from_group returns a list of netbird rules for a given group_id +rules_from_group(group_id, direction, action, port) := rules if { group := input.groups[_] group.ID == group_id - peers := [peer | peer := group.Peers[_]] -} - -# netbird_rules_from_groups returns a list of netbird rules for a given list of group names -rules_from_groups(groups, direction, action, port) := rules if { - group_id := groups[_] - rules := [get_rule(peer, direction, action, port) | peer := peers_from_group(group_id)[_]] + rules := [get_rule(peer, direction, action, port) | peer := group.Peers[_]] } # is_peer_in_any_group checks that input peer present at least in one group