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.
This commit is contained in:
Givi Khojanashvili 2023-04-01 14:02:08 +04:00 committed by GitHub
parent 909f305728
commit 5dc0ff42a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 315 additions and 80 deletions

2
go.mod
View File

@ -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

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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])
}
})
}

View File

@ -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}}
}[_][_]
}

View File

@ -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