Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Policy engine cleanup #534

Merged
merged 1 commit into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
# the repo. Unless a later match takes precedence,
# Order is important; the last matching pattern takes the most
# precedence.
* @elevran
cmd/ @kfirtoledo @orozery
demos/ @kfirtoledo
pkg/ @elevran @kfirtoledo @orozery
pkg/dataplane/go @praveingk @orozery
pkg/policyengine/ @zivnevo
website/ @elevran @michalmalka
* @elevran
cmd/ @kfirtoledo @orozery
demos/ @kfirtoledo
pkg/ @elevran @kfirtoledo @orozery
pkg/dataplane/go @praveingk @orozery
pkg/controlplane/authz/ @zivnevo
website/ @elevran @michalmalka

14 changes: 10 additions & 4 deletions cmd/cl-controlplane/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func (o *Options) Run() error {
httpServer := utilrest.NewServer("controlplane-http", parsedCertData.ServerConfig())
grpcServer := grpc.NewServer("controlplane-grpc", parsedCertData.ServerConfig())

authzManager, err := authz.NewManager(parsedCertData)
authzManager, err := authz.NewManager(parsedCertData, mgr.GetClient(), namespace)
if err != nil {
return fmt.Errorf("cannot create authorization manager: %w", err)
}
Expand Down Expand Up @@ -240,10 +240,16 @@ func (o *Options) Run() error {

cprest.RegisterHandlers(restManager, httpServer)

controlManager.SetGetMergeImportListCallback(restManager.GetMergeImportList)
authzManager.SetGetImportCallback(restManager.GetK8sImport)
authzManager.SetGetExportCallback(restManager.GetK8sExport)
authzManager.SetGetPeerCallback(restManager.GetK8sPeer)
controlManager.SetGetImportCallback(restManager.GetK8sImport)
controlManager.SetStatusCallback(func(pr *v1alpha1.Peer) {
authzManager.AddPeer(pr)
controlManager.SetGetMergeImportListCallback(restManager.GetMergeImportList)
controlManager.SetPeerStatusCallback(func(pr *v1alpha1.Peer) {
restManager.UpdatePeerStatus(pr.Name, &pr.Status)
})
controlManager.SetExportStatusCallback(func(export *v1alpha1.Export) {
restManager.UpdateExportStatus(export.Name, &export.Status)
})
}

Expand Down
8 changes: 4 additions & 4 deletions demos/iperf3/kind/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,16 +164,16 @@ When running Kind cluster on macOS run instead the following:
### Step 7: Create access policy
In this step, we create a policy that allow to all traffic from peer1 and peer2:

gwctl --myid peer1 create policy --type access --policyFile $PROJECT_DIR/pkg/policyengine/examples/allowAll.json
gwctl --myid peer2 create policy --type access --policyFile $PROJECT_DIR/pkg/policyengine/examples/allowAll.json
gwctl --myid peer1 create policy --type access --policyFile $PROJECT_DIR/examples/policies/allowAll.json
gwctl --myid peer2 create policy --type access --policyFile $PROJECT_DIR/examples/policies/allowAll.json

When running Kind cluster on macOS run instead the following:

kubectl config use-context kind-peer1
kubectl cp $PROJECT_DIR/pkg/policyengine/examples/allowAll.json gwctl:/tmp/allowAll.json
kubectl cp $PROJECT_DIR/examples/policies/allowAll.json gwctl:/tmp/allowAll.json
kubectl exec -i $GWCTL1 -- gwctl create policy --type access --policyFile /tmp/allowAll.json
kubectl config use-context kind-peer2
kubectl cp $PROJECT_DIR/pkg/policyengine/examples/allowAll.json gwctl:/tmp/allowAll.json
kubectl cp $PROJECT_DIR/examples/policies//allowAll.json gwctl:/tmp/allowAll.json
kubectl exec -i $GWCTL2 -- gwctl create policy --type access --policyFile /tmp/allowAll.json

### Final Step : Test Service connectivity
Expand Down
14 changes: 12 additions & 2 deletions pkg/apis/clusterlink.net/v1alpha1/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ type ImportSource struct {
ExportNamespace string `json:"exportNamespace"`
}

// LBScheme represents a load balancing scheme.
type LBScheme string

const (
LBSchemeRandom LBScheme = "random"
LBSchemeRoundRobin LBScheme = "round-robin"
LBSchemeStatic LBScheme = "static"

LBSchemeDefault = LBSchemeRoundRobin
)

// ImportSpec contains all attributes of an imported service.
type ImportSpec struct {
// Port of the imported service.
Expand All @@ -53,8 +64,7 @@ type ImportSpec struct {
Sources []ImportSource `json:"sources"`
// +kubebuilder:default="round-robin"
// LBScheme is the load-balancing scheme to use (e.g., random, static, round-robin)
LBScheme string `json:"lbScheme"`
// TODO: Make LBScheme a proper type (when backwards compatibility is no longer needed)
LBScheme LBScheme `json:"lbScheme"`
}

const (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,45 +127,32 @@ func (pdp *PDP) DeletePolicy(policyName types.NamespacedName, privileged bool) e
return pdp.regularPolicies.deletePolicy(policyName)
}

// Decide makes allow/deny decisions for the queried connections between src and each of destinations in dests.
// The decision, as well as the deciding policy, are recorded in the returned slice of DestinationDecision structs.
// The order of destinations in dests is preserved in the returned slice.
func (pdp *PDP) Decide(src WorkloadAttrs, dests []WorkloadAttrs, ns string) ([]DestinationDecision, error) {
decisions := make([]DestinationDecision, len(dests))
for i, dest := range dests {
decisions[i] = DestinationDecision{Destination: dest}
}
// Decide makes allow/deny decisions for the queried connection between src and dest.
// The decision, as well as the deciding policy, is recorded in the returned DestinationDecision struct.
func (pdp *PDP) Decide(src, dest WorkloadAttrs, ns string) (*DestinationDecision, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: why is ns not part of either source or destination attributes?

decision := DestinationDecision{Destination: dest}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: are we ok with the default values of the other fields?


allDestsDecided, err := pdp.privilegedPolicies.decide(src, decisions, ns)
decided, err := pdp.privilegedPolicies.decide(src, &decision, ns)
if err != nil {
return nil, err
}
if allDestsDecided {
return decisions, nil
if decided {
return &decision, nil
}

allDestsDecided, err = pdp.regularPolicies.decide(src, decisions, ns)
decided, err = pdp.regularPolicies.decide(src, &decision, ns)
if err != nil {
return nil, err
}
if allDestsDecided {
return decisions, nil
if decided {
return &decision, nil
}

// For all undecided destination (for which no policy matched) set the default deny action
denyUndecidedDestinations(decisions)
return decisions, nil
}

func denyUndecidedDestinations(dest []DestinationDecision) {
for i := range dest {
dd := &dest[i]
if dd.Decision == DecisionUndecided {
dd.Decision = DecisionDeny
dd.MatchedBy = DefaultDenyPolicyName
dd.PrivilegedMatch = false
}
}
// for an undecided destination (no policy matched) set the default deny action
decision.Decision = DecisionDeny
decision.MatchedBy = DefaultDenyPolicyName
decision.PrivilegedMatch = false
return &decision, nil
}

func newPolicyTier(privileged bool) policyTier {
Expand Down Expand Up @@ -223,62 +210,53 @@ func (pt *policyTier) unsafeDeletePolicy(policyName types.NamespacedName) error
return nil
}

// decide first checks whether any of the tier's deny policies matches any of the not-yet-decided connections
// between src and each of the destinations in dests. If one policy does, the relevant DestinationDecision will
// decide first checks whether any of the tier's deny policies matches any of the not-yet-decided connection
// between src and dest. If one policy does, the DestinationDecision will
// be updated to reflect the connection been denied.
// The function then checks whether any of the tier's allow policies matches any of the remaining undecided connections,
// and will similarly update the relevant DestinationDecision of any matching connection.
// returns whether all destinations were decided and an error (if occurred).
func (pt *policyTier) decide(src WorkloadAttrs, dests []DestinationDecision, ns string) (bool, error) {
// If the connection is not decided, the function then checks whether any of the tier's allow policies matches,
// and will similarly update the DestinationDecision.
// returns whether the destination was decided and an error (if occurred).
func (pt *policyTier) decide(src WorkloadAttrs, dest *DestinationDecision, ns string) (bool, error) {
pt.lock.RLock() // allowing multiple simultaneous calls to decide() to be served
defer pt.lock.RUnlock()
allDecided, err := pt.denyPolicies.decide(src, dests, pt.privileged, ns)
decided, err := pt.denyPolicies.decide(src, dest, pt.privileged, ns)
if err != nil {
return false, err
}
if allDecided {
if decided {
return true, nil
}

allDecided, err = pt.allowPolicies.decide(src, dests, pt.privileged, ns)
decided, err = pt.allowPolicies.decide(src, dest, pt.privileged, ns)
if err != nil {
return false, err
}
return allDecided, nil
return decided, nil
}

// decide iterates over all policies in a connPolicyMap and checks if they make a connectivity decision (allow/deny)
// on the not-yet-decided connections between src and each of the destinations in dests.
// returns whether all destinations were decided and an error (if occurred).
func (cpm connPolicyMap) decide(src WorkloadAttrs, dests []DestinationDecision, privileged bool, ns string) (bool, error) {
// on the not-yet-decided connection between src and dest.
// returns whether the destination was decided and an error (if occurred).
func (cpm connPolicyMap) decide(src WorkloadAttrs, dest *DestinationDecision, privileged bool, ns string) (bool, error) {
// for when there are no policies in cpm (some destinations are undecided, otherwise we shouldn't be here)
allDecided := false
for policyName, policy := range cpm {
if !privileged && policyName.Namespace != ns { // Only consider non-privileged policies from the given namespace
continue
}
allDecided = true // assume all destinations were decided, unless we find a destination which is not
for i := range dests {
dest := &dests[i]
if dest.Decision == DecisionUndecided {
decision, err := accessPolicyDecide(policy, src, dest.Destination)
if err != nil {
return false, err
}
if decision == DecisionUndecided {
allDecided = false // policy didn't match dest - not all dests are decided
} else { // policy matched - we now have a decision for dest
dest.Decision = decision
dest.MatchedBy = policyName.String()
dest.PrivilegedMatch = privileged
}
}

decision, err := accessPolicyDecide(policy, src, dest.Destination)
if err != nil {
return false, err
}
if allDecided {
break
if decision != DecisionUndecided { // policy matched - we now have a decision for dest
dest.Decision = decision
dest.MatchedBy = policyName.String()
dest.PrivilegedMatch = privileged
return true, nil
}
}
return allDecided, nil

return false, nil
}

// accessPolicyDecide returns a policy's decision on a given connection.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
"k8s.io/apimachinery/pkg/util/yaml"

"github.com/clusterlink-net/clusterlink/pkg/apis/clusterlink.net/v1alpha1"
"github.com/clusterlink-net/clusterlink/pkg/policyengine/connectivitypdp"
"github.com/clusterlink-net/clusterlink/pkg/controlplane/authz/connectivitypdp"
)

const (
Expand Down Expand Up @@ -66,30 +66,27 @@ func TestPrivilegedVsRegular(t *testing.T) {
}

pdp := connectivitypdp.NewPDP()
dests := []connectivitypdp.WorkloadAttrs{trivialLabel}
decisions, err := pdp.Decide(trivialLabel, dests, defaultNS)
decision, err := pdp.Decide(trivialLabel, trivialLabel, defaultNS)
require.Nil(t, err)
require.Equal(t, connectivitypdp.DecisionDeny, decisions[0].Decision) // default deny
require.Equal(t, connectivitypdp.DefaultDenyPolicyName, decisions[0].MatchedBy)
require.Equal(t, false, decisions[0].PrivilegedMatch)
require.Equal(t, connectivitypdp.DecisionDeny, decision.Decision) // default deny
require.Equal(t, connectivitypdp.DefaultDenyPolicyName, decision.MatchedBy)
require.Equal(t, false, decision.PrivilegedMatch)

err = pdp.AddOrUpdatePolicy(connectivitypdp.PolicyFromCR(&trivialConnPol))
require.Nil(t, err)
dests = []connectivitypdp.WorkloadAttrs{trivialLabel}
decisions, err = pdp.Decide(trivialLabel, dests, defaultNS)
decision, err = pdp.Decide(trivialLabel, trivialLabel, defaultNS)
require.Nil(t, err)
require.Equal(t, connectivitypdp.DecisionAllow, decisions[0].Decision) // regular allow policy allows connection
require.Equal(t, types.NamespacedName{Name: "reg", Namespace: defaultNS}.String(), decisions[0].MatchedBy)
require.Equal(t, false, decisions[0].PrivilegedMatch)
require.Equal(t, connectivitypdp.DecisionAllow, decision.Decision) // regular allow policy allows connection
require.Equal(t, types.NamespacedName{Name: "reg", Namespace: defaultNS}.String(), decision.MatchedBy)
require.Equal(t, false, decision.PrivilegedMatch)

err = pdp.AddOrUpdatePolicy(connectivitypdp.PolicyFromPrivilegedCR(&trivialPrivConnPol))
require.Nil(t, err)
dests = []connectivitypdp.WorkloadAttrs{trivialLabel}
decisions, err = pdp.Decide(trivialLabel, dests, defaultNS)
decision, err = pdp.Decide(trivialLabel, trivialLabel, defaultNS)
require.Nil(t, err)
require.Equal(t, connectivitypdp.DecisionDeny, decisions[0].Decision) // privileged deny policy denies connection
require.Equal(t, types.NamespacedName{Name: "priv"}.String(), decisions[0].MatchedBy)
require.Equal(t, true, decisions[0].PrivilegedMatch)
require.Equal(t, connectivitypdp.DecisionDeny, decision.Decision) // privileged deny policy denies connection
require.Equal(t, types.NamespacedName{Name: "priv"}.String(), decision.MatchedBy)
require.Equal(t, true, decision.PrivilegedMatch)
}

// TestAllLayers starts with one policy per layer (allow/deny X privileged/non-privileged)
Expand All @@ -103,66 +100,69 @@ func TestAllLayers(t *testing.T) {
err := addPoliciesFromFile(pdp, fileInTestDir("all_layers.yaml"))
require.Nil(t, err)

decision, err := pdp.Decide(trivialLabel, trivialLabel, defaultNS)
require.Nil(t, err)
require.Equal(t, connectivitypdp.DecisionDeny, decision.Decision) // default deny
require.Equal(t, connectivitypdp.DefaultDenyPolicyName, decision.MatchedBy)
require.Equal(t, false, decision.PrivilegedMatch)
require.Equal(t, trivialLabel, decision.Destination)

nonMeteringLabel := connectivitypdp.WorkloadAttrs{"workloadName": "non-metering-service"}
decision, err = pdp.Decide(trivialLabel, nonMeteringLabel, defaultNS)
require.Nil(t, err)
require.Equal(t, connectivitypdp.DecisionAllow, decision.Decision) // regular allow
require.Equal(t, false, decision.PrivilegedMatch)

meteringLabel := connectivitypdp.WorkloadAttrs{"workloadName": "global-metering-service"}
decision, err = pdp.Decide(trivialLabel, meteringLabel, defaultNS)
require.Nil(t, err)
require.Equal(t, connectivitypdp.DecisionDeny, decision.Decision) // regular deny
require.Equal(t, false, decision.PrivilegedMatch)

privateMeteringLabel := connectivitypdp.WorkloadAttrs{"workloadName": "global-metering-service", "environment": "prod"}
dests := []connectivitypdp.WorkloadAttrs{trivialLabel, nonMeteringLabel, meteringLabel, privateMeteringLabel}
decisions, err := pdp.Decide(trivialLabel, dests, defaultNS)
decision, err = pdp.Decide(trivialLabel, privateMeteringLabel, defaultNS)
require.Nil(t, err)
require.Equal(t, connectivitypdp.DecisionDeny, decisions[0].Decision) // default deny
require.Equal(t, connectivitypdp.DefaultDenyPolicyName, decisions[0].MatchedBy)
require.Equal(t, false, decisions[0].PrivilegedMatch)
require.Equal(t, trivialLabel, decisions[0].Destination)
require.Equal(t, connectivitypdp.DecisionAllow, decisions[1].Decision) // regular allow
require.Equal(t, false, decisions[1].PrivilegedMatch)
require.Equal(t, connectivitypdp.DecisionDeny, decisions[2].Decision) // regular deny
require.Equal(t, false, decisions[2].PrivilegedMatch)
require.Equal(t, connectivitypdp.DecisionAllow, decisions[3].Decision) // privileged allow
require.Equal(t, true, decisions[3].PrivilegedMatch)
require.Equal(t, connectivitypdp.DecisionAllow, decision.Decision) // privileged allow
require.Equal(t, true, decision.PrivilegedMatch)

privateLabel := map[string]string{"classification": "private", "environment": "prod"}
dests = []connectivitypdp.WorkloadAttrs{privateMeteringLabel}
decisions, err = pdp.Decide(privateLabel, dests, defaultNS)
decision, err = pdp.Decide(privateLabel, privateMeteringLabel, defaultNS)
require.Nil(t, err)
require.Equal(t, connectivitypdp.DecisionDeny, decisions[0].Decision) // privileged deny
require.Equal(t, true, decisions[0].PrivilegedMatch)
require.Equal(t, connectivitypdp.DecisionDeny, decision.Decision) // privileged deny
require.Equal(t, true, decision.PrivilegedMatch)

privDenyPolicy := getNameOfFirstPolicyInPDP(pdp, v1alpha1.AccessPolicyActionDeny, true)
require.NotEmpty(t, privDenyPolicy)
err = pdp.DeletePolicy(types.NamespacedName{Name: privDenyPolicy}, true)
require.Nil(t, err)
dests = []connectivitypdp.WorkloadAttrs{privateMeteringLabel}
decisions, err = pdp.Decide(privateLabel, dests, defaultNS)
decision, err = pdp.Decide(privateLabel, privateMeteringLabel, defaultNS)
require.Nil(t, err)
// no privileged deny, so privileged allow matches
require.Equal(t, connectivitypdp.DecisionAllow, decisions[0].Decision)
require.Equal(t, connectivitypdp.DecisionAllow, decision.Decision)

privAllowPolicy := getNameOfFirstPolicyInPDP(pdp, v1alpha1.AccessPolicyActionAllow, true)
require.NotEmpty(t, privAllowPolicy)
err = pdp.DeletePolicy(types.NamespacedName{Name: privAllowPolicy}, true)
require.Nil(t, err)
dests = []connectivitypdp.WorkloadAttrs{privateMeteringLabel}
decisions, err = pdp.Decide(privateLabel, dests, defaultNS)
decision, err = pdp.Decide(privateLabel, privateMeteringLabel, defaultNS)
require.Nil(t, err)
require.Equal(t, connectivitypdp.DecisionDeny, decisions[0].Decision) // no privileged allow, so regular deny matches
require.Equal(t, connectivitypdp.DecisionDeny, decision.Decision) // no privileged allow, so regular deny matches

regDenyPolicy := getNameOfFirstPolicyInPDP(pdp, v1alpha1.AccessPolicyActionDeny, false)
require.NotEmpty(t, regDenyPolicy)
err = pdp.DeletePolicy(types.NamespacedName{Name: regDenyPolicy, Namespace: defaultNS}, false)
require.Nil(t, err)
dests = []connectivitypdp.WorkloadAttrs{privateMeteringLabel}
decisions, err = pdp.Decide(privateLabel, dests, defaultNS)
decision, err = pdp.Decide(privateLabel, privateMeteringLabel, defaultNS)
require.Nil(t, err)
require.Equal(t, connectivitypdp.DecisionAllow, decisions[0].Decision) // no regular deny, so regular allow matches
require.Equal(t, connectivitypdp.DecisionAllow, decision.Decision) // no regular deny, so regular allow matches

regAllowPolicy := getNameOfFirstPolicyInPDP(pdp, v1alpha1.AccessPolicyActionAllow, false)
require.NotEmpty(t, regAllowPolicy)
err = pdp.DeletePolicy(types.NamespacedName{Name: regAllowPolicy, Namespace: defaultNS}, false)
require.Nil(t, err)
dests = []connectivitypdp.WorkloadAttrs{privateMeteringLabel}
decisions, err = pdp.Decide(privateLabel, dests, defaultNS)
decision, err = pdp.Decide(privateLabel, privateMeteringLabel, defaultNS)
require.Nil(t, err)
require.Equal(t, connectivitypdp.DecisionDeny, decisions[0].Decision) // no regular allow, so default deny matches
require.Equal(t, connectivitypdp.DecisionDeny, decision.Decision) // no regular allow, so default deny matches
}

func getNameOfFirstPolicyInPDP(pdp *connectivitypdp.PDP, action v1alpha1.AccessPolicyAction, privileged bool) string {
Expand Down
Loading
Loading