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