From fa94682e83ed7c6658d17838cd6c97a3ed725e8f Mon Sep 17 00:00:00 2001 From: Anis Elleuch Date: Tue, 16 Mar 2021 16:50:36 +0100 Subject: [PATCH] policy: Add Merge API (#11793) This commit adds a new API in pkg/bucket/policy package called Merge to merge multiple policies of a user or a group into one policy document. --- pkg/bucket/policy/actionset.go | 6 +- pkg/bucket/policy/condition/func.go | 36 ++++++++++ pkg/bucket/policy/condition/valueset.go | 14 ++++ pkg/bucket/policy/policy.go | 37 +++++----- pkg/bucket/policy/policy_test.go | 94 +++++++++++++++++++++++++ pkg/bucket/policy/principal.go | 6 ++ pkg/bucket/policy/resourceset.go | 15 ++++ pkg/bucket/policy/statement.go | 26 +++++++ pkg/iam/policy/actionset.go | 5 ++ pkg/iam/policy/policy.go | 33 +++++---- pkg/iam/policy/resourceset.go | 15 ++++ pkg/iam/policy/statement.go | 23 ++++++ 12 files changed, 279 insertions(+), 31 deletions(-) diff --git a/pkg/bucket/policy/actionset.go b/pkg/bucket/policy/actionset.go index 7abefc42e..fdb1f7e68 100644 --- a/pkg/bucket/policy/actionset.go +++ b/pkg/bucket/policy/actionset.go @@ -93,10 +93,14 @@ func (actionSet ActionSet) ToSlice() []Action { for action := range actionSet { actions = append(actions, action) } - return actions } +// Clone clones ActionSet structure +func (actionSet ActionSet) Clone() ActionSet { + return NewActionSet(actionSet.ToSlice()...) +} + // UnmarshalJSON - decodes JSON data to ActionSet. func (actionSet *ActionSet) UnmarshalJSON(data []byte) error { var sset set.StringSet diff --git a/pkg/bucket/policy/condition/func.go b/pkg/bucket/policy/condition/func.go index 08a368b37..3304d5fac 100644 --- a/pkg/bucket/policy/condition/func.go +++ b/pkg/bucket/policy/condition/func.go @@ -66,6 +66,42 @@ func (functions Functions) Keys() KeySet { return keySet } +// Clone clones Functions structure +func (functions Functions) Clone() Functions { + funcs := []Function{} + + for _, f := range functions { + vfn := conditionFuncMap[f.name()] + for key, values := range f.toMap() { + function, _ := vfn(key, values.Clone()) + funcs = append(funcs, function) + } + } + + return funcs +} + +// Equals returns true if two Functions structures are equal +func (functions Functions) Equals(funcs Functions) bool { + if len(functions) != len(funcs) { + return false + } + for _, fi := range functions { + fistr := fi.String() + found := false + for _, fj := range funcs { + if fistr == fj.String() { + found = true + break + } + } + if !found { + return false + } + } + return true +} + // MarshalJSON - encodes Functions to JSON data. func (functions Functions) MarshalJSON() ([]byte, error) { nm := make(map[name]map[Key]ValueSet) diff --git a/pkg/bucket/policy/condition/valueset.go b/pkg/bucket/policy/condition/valueset.go index 90e8b26af..afdf2e128 100644 --- a/pkg/bucket/policy/condition/valueset.go +++ b/pkg/bucket/policy/condition/valueset.go @@ -29,6 +29,15 @@ func (set ValueSet) Add(value Value) { set[value] = struct{}{} } +// ToSlice converts ValueSet to a slice of Value +func (set ValueSet) ToSlice() []Value { + var values []Value + for k := range set { + values = append(values, k) + } + return values +} + // MarshalJSON - encodes ValueSet to JSON data. func (set ValueSet) MarshalJSON() ([]byte, error) { var values []Value @@ -73,6 +82,11 @@ func (set *ValueSet) UnmarshalJSON(data []byte) error { return nil } +// Clone clones ValueSet structure +func (set ValueSet) Clone() ValueSet { + return NewValueSet(set.ToSlice()...) +} + // NewValueSet - returns new value set containing given values. func NewValueSet(values ...Value) ValueSet { set := make(ValueSet) diff --git a/pkg/bucket/policy/policy.go b/pkg/bucket/policy/policy.go index 59bdcf999..4defbc44e 100644 --- a/pkg/bucket/policy/policy.go +++ b/pkg/bucket/policy/policy.go @@ -100,27 +100,30 @@ func (policy Policy) MarshalJSON() ([]byte, error) { return json.Marshal(subPolicy(policy)) } +// Merge merges two policies documents and drop +// duplicate statements if any. +func (policy Policy) Merge(input Policy) Policy { + var mergedPolicy Policy + if policy.Version != "" { + mergedPolicy.Version = policy.Version + } else { + mergedPolicy.Version = input.Version + } + for _, st := range policy.Statements { + mergedPolicy.Statements = append(mergedPolicy.Statements, st.Clone()) + } + for _, st := range input.Statements { + mergedPolicy.Statements = append(mergedPolicy.Statements, st.Clone()) + } + mergedPolicy.dropDuplicateStatements() + return mergedPolicy +} + func (policy *Policy) dropDuplicateStatements() { redo: for i := range policy.Statements { for j, statement := range policy.Statements[i+1:] { - if policy.Statements[i].Effect != statement.Effect { - continue - } - - if !policy.Statements[i].Principal.Equals(statement.Principal) { - continue - } - - if !policy.Statements[i].Actions.Equals(statement.Actions) { - continue - } - - if !policy.Statements[i].Resources.Equals(statement.Resources) { - continue - } - - if policy.Statements[i].Conditions.String() != statement.Conditions.String() { + if !policy.Statements[i].Equals(statement) { continue } policy.Statements = append(policy.Statements[:j], policy.Statements[j+1:]...) diff --git a/pkg/bucket/policy/policy_test.go b/pkg/bucket/policy/policy_test.go index 039dd6126..05629df6f 100644 --- a/pkg/bucket/policy/policy_test.go +++ b/pkg/bucket/policy/policy_test.go @@ -1144,3 +1144,97 @@ func TestPolicyValidate(t *testing.T) { } } } + +func TestPolicyMerge(t *testing.T) { + testCases := []struct { + policy string + }{ + {`{ + "Version": "2012-10-17", + "Id": "S3PolicyId1", + "Statement": [ + { + "Sid": "statement1", + "Effect": "Deny", + "Principal": "*", + "Action":["s3:GetObject", "s3:PutObject"], + "Resource": "arn:aws:s3:::awsexamplebucket1/*" + } + ] +}`}, + {`{ + "Version": "2012-10-17", + "Id": "S3PolicyId1", + "Statement": [ + { + "Sid": "statement1", + "Effect": "Allow", + "Principal": "*", + "Action":"s3:GetObject", + "Resource": "arn:aws:s3:::awsexamplebucket1/*", + "Condition" : { + "IpAddress" : { + "aws:SourceIp": "192.0.2.0/24" + }, + "NotIpAddress" : { + "aws:SourceIp": "192.0.2.188/32" + } + } + } + ] +}`}, + {`{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "cross-account permission to user in your own account", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::123456789012:user/Dave" + }, + "Action": "s3:PutObject", + "Resource": "arn:aws:s3:::awsexamplebucket1/*" + }, + { + "Sid": "Deny your user permission to upload object if copy source is not /bucket/folder", + "Effect": "Deny", + "Principal": { + "AWS": "arn:aws:iam::123456789012:user/Dave" + }, + "Action": "s3:PutObject", + "Resource": "arn:aws:s3:::awsexamplebucket1/*", + "Condition": { + "StringNotLike": { + "s3:x-amz-copy-source": "awsexamplebucket1/public/*" + } + } + } + ] +}`}, + } + + for i, testCase := range testCases { + var p Policy + err := json.Unmarshal([]byte(testCase.policy), &p) + if err != nil { + t.Fatalf("case %v: unexpected error: %v", i+1, err) + } + + var clonedPolicy Policy + clonedPolicy = clonedPolicy.Merge(p) + + j, err := json.Marshal(clonedPolicy) + if err != nil { + t.Fatalf("case %v: unexpected error: %v", i+1, err) + } + + err = json.Unmarshal(j, &clonedPolicy) + if err != nil { + t.Fatalf("case %v: unexpected error: %v", i+1, err) + } + + if !clonedPolicy.Statements[0].Equals(p.Statements[0]) { + t.Fatalf("case %v: different policy outcome", i+1) + } + } +} diff --git a/pkg/bucket/policy/principal.go b/pkg/bucket/policy/principal.go index 9fc62e653..51f71cae8 100644 --- a/pkg/bucket/policy/principal.go +++ b/pkg/bucket/policy/principal.go @@ -90,6 +90,12 @@ func (p *Principal) UnmarshalJSON(data []byte) error { return nil } +// Clone clones Principal structure +func (p Principal) Clone() Principal { + return NewPrincipal(p.AWS.ToSlice()...) + +} + // NewPrincipal - creates new Principal. func NewPrincipal(principals ...string) Principal { return Principal{AWS: set.CreateStringSet(principals...)} diff --git a/pkg/bucket/policy/resourceset.go b/pkg/bucket/policy/resourceset.go index d23d2aa68..375eeea01 100644 --- a/pkg/bucket/policy/resourceset.go +++ b/pkg/bucket/policy/resourceset.go @@ -154,6 +154,21 @@ func (resourceSet ResourceSet) Validate(bucketName string) error { return nil } +// ToSlice - returns slice of resources from the resource set. +func (resourceSet ResourceSet) ToSlice() []Resource { + resources := []Resource{} + for resource := range resourceSet { + resources = append(resources, resource) + } + + return resources +} + +// Clone clones ResourceSet structure +func (resourceSet ResourceSet) Clone() ResourceSet { + return NewResourceSet(resourceSet.ToSlice()...) +} + // NewResourceSet - creates new resource set. func NewResourceSet(resources ...Resource) ResourceSet { resourceSet := make(ResourceSet) diff --git a/pkg/bucket/policy/statement.go b/pkg/bucket/policy/statement.go index 5b34f98af..0b16cfa8c 100644 --- a/pkg/bucket/policy/statement.go +++ b/pkg/bucket/policy/statement.go @@ -33,6 +33,26 @@ type Statement struct { Conditions condition.Functions `json:"Condition,omitempty"` } +// Equals checks if two statements are equal +func (statement Statement) Equals(st Statement) bool { + if statement.Effect != st.Effect { + return false + } + if !statement.Principal.Equals(st.Principal) { + return false + } + if !statement.Actions.Equals(st.Actions) { + return false + } + if !statement.Resources.Equals(st.Resources) { + return false + } + if !statement.Conditions.Equals(st.Conditions) { + return false + } + return true +} + // IsAllowed - checks given policy args is allowed to continue the Rest API. func (statement Statement) IsAllowed(args Args) bool { check := func() bool { @@ -143,6 +163,12 @@ func (statement Statement) Validate(bucketName string) error { return statement.Resources.Validate(bucketName) } +// Clone clones Statement structure +func (statement Statement) Clone() Statement { + return NewStatement(statement.Effect, statement.Principal.Clone(), + statement.Actions.Clone(), statement.Resources.Clone(), statement.Conditions.Clone()) +} + // NewStatement - creates new statement. func NewStatement(effect Effect, principal Principal, actionSet ActionSet, resourceSet ResourceSet, conditions condition.Functions) Statement { return Statement{ diff --git a/pkg/iam/policy/actionset.go b/pkg/iam/policy/actionset.go index ac75d9879..1eef73fa7 100644 --- a/pkg/iam/policy/actionset.go +++ b/pkg/iam/policy/actionset.go @@ -27,6 +27,11 @@ import ( // ActionSet - set of actions. type ActionSet map[Action]struct{} +// Clone clones ActionSet structure +func (actionSet ActionSet) Clone() ActionSet { + return NewActionSet(actionSet.ToSlice()...) +} + // Add - add action to the set. func (actionSet ActionSet) Add(action Action) { actionSet[action] = struct{}{} diff --git a/pkg/iam/policy/policy.go b/pkg/iam/policy/policy.go index 6c17e5a1b..230b68dc1 100644 --- a/pkg/iam/policy/policy.go +++ b/pkg/iam/policy/policy.go @@ -151,23 +151,30 @@ func (iamp Policy) isValid() error { return nil } +// Merge merges two policies documents and drop +// duplicate statements if any. +func (iamp Policy) Merge(input Policy) Policy { + var mergedPolicy Policy + if iamp.Version != "" { + mergedPolicy.Version = iamp.Version + } else { + mergedPolicy.Version = input.Version + } + for _, st := range iamp.Statements { + mergedPolicy.Statements = append(mergedPolicy.Statements, st.Clone()) + } + for _, st := range input.Statements { + mergedPolicy.Statements = append(mergedPolicy.Statements, st.Clone()) + } + mergedPolicy.dropDuplicateStatements() + return mergedPolicy +} + func (iamp *Policy) dropDuplicateStatements() { redo: for i := range iamp.Statements { for j, statement := range iamp.Statements[i+1:] { - if iamp.Statements[i].Effect != statement.Effect { - continue - } - - if !iamp.Statements[i].Actions.Equals(statement.Actions) { - continue - } - - if !iamp.Statements[i].Resources.Equals(statement.Resources) { - continue - } - - if iamp.Statements[i].Conditions.String() != statement.Conditions.String() { + if !iamp.Statements[i].Equals(statement) { continue } iamp.Statements = append(iamp.Statements[:j], iamp.Statements[j+1:]...) diff --git a/pkg/iam/policy/resourceset.go b/pkg/iam/policy/resourceset.go index 2e357dc8e..4d71df231 100644 --- a/pkg/iam/policy/resourceset.go +++ b/pkg/iam/policy/resourceset.go @@ -154,6 +154,21 @@ func (resourceSet ResourceSet) Validate() error { return nil } +// ToSlice - returns slice of resources from the resource set. +func (resourceSet ResourceSet) ToSlice() []Resource { + resources := []Resource{} + for resource := range resourceSet { + resources = append(resources, resource) + } + + return resources +} + +// Clone clones ResourceSet structure +func (resourceSet ResourceSet) Clone() ResourceSet { + return NewResourceSet(resourceSet.ToSlice()...) +} + // NewResourceSet - creates new resource set. func NewResourceSet(resources ...Resource) ResourceSet { resourceSet := make(ResourceSet) diff --git a/pkg/iam/policy/statement.go b/pkg/iam/policy/statement.go index 2eca339ca..03ff824e7 100644 --- a/pkg/iam/policy/statement.go +++ b/pkg/iam/policy/statement.go @@ -129,6 +129,29 @@ func (statement Statement) Validate() error { return statement.isValid() } +// Equals checks if two statements are equal +func (statement Statement) Equals(st Statement) bool { + if statement.Effect != st.Effect { + return false + } + if !statement.Actions.Equals(st.Actions) { + return false + } + if !statement.Resources.Equals(st.Resources) { + return false + } + if !statement.Conditions.Equals(st.Conditions) { + return false + } + return true +} + +// Clone clones Statement structure +func (statement Statement) Clone() Statement { + return NewStatement(statement.Effect, statement.Actions.Clone(), + statement.Resources.Clone(), statement.Conditions.Clone()) +} + // NewStatement - creates new statement. func NewStatement(effect policy.Effect, actionSet ActionSet, resourceSet ResourceSet, conditions condition.Functions) Statement { return Statement{