[#22] iam: Fix converters

Validate that actions and resources contain wildcard only at the end

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
Denis Kirillov 2023-11-28 17:56:36 +03:00
parent 5fa9d91903
commit a0a35bf4bf
4 changed files with 255 additions and 17 deletions

View file

@ -6,6 +6,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode/utf8"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
) )
@ -63,6 +64,9 @@ var (
// ErrInvalidResourceFormat occurs when resource has unknown/unsupported format. // ErrInvalidResourceFormat occurs when resource has unknown/unsupported format.
ErrInvalidResourceFormat = errors.New("invalid resource format") ErrInvalidResourceFormat = errors.New("invalid resource format")
// ErrInvalidActionFormat occurs when action has unknown/unsupported format.
ErrInvalidActionFormat = errors.New("invalid action format")
) )
type formPrincipalConditionFunc func(string) chain.Condition type formPrincipalConditionFunc func(string) chain.Condition
@ -240,6 +244,10 @@ func parsePrincipalAsIAMUser(principal string) (account string, user string, err
} }
func parseResourceAsS3ARN(resource string) (bucket string, object string, err error) { func parseResourceAsS3ARN(resource string) (bucket string, object string, err error) {
if resource == Wildcard {
return Wildcard, Wildcard, nil
}
if !strings.HasPrefix(resource, s3ResourcePrefix) { if !strings.HasPrefix(resource, s3ResourcePrefix) {
return "", "", ErrInvalidResourceFormat return "", "", ErrInvalidResourceFormat
} }
@ -264,6 +272,26 @@ func parseResourceAsS3ARN(resource string) (bucket string, object string, err er
return bucket, object, nil return bucket, object, nil
} }
func parseActionAsS3Action(action string) (string, error) {
if action == Wildcard {
return Wildcard, nil
}
if !strings.HasPrefix(action, s3ActionPrefix) {
return "", ErrInvalidActionFormat
}
// iam arn format :s3:<action-name>
s3Action := strings.TrimPrefix(action, s3ActionPrefix)
index := strings.IndexByte(s3Action, Wildcard[0])
if index != -1 && index != utf8.RuneCountInString(s3Action)-1 {
return "", ErrInvalidActionFormat
}
return s3Action, nil
}
func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition { func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition {
var orConditions []chain.Condition var orConditions []chain.Condition
commonConditions := make([]chain.Condition, 0, len(groupedConditions)) commonConditions := make([]chain.Condition, 0, len(groupedConditions))

View file

@ -3,7 +3,6 @@ package iam
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native" "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
@ -46,7 +45,11 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro
status := formStatus(statement) status := formStatus(statement)
action, actionInverted := statement.action() action, actionInverted := statement.action()
ruleAction := chain.Actions{Inverted: actionInverted, Names: formNativeActionNames(action)} nativeActions, err := formNativeActionNames(action)
if err != nil {
return nil, err
}
ruleAction := chain.Actions{Inverted: actionInverted, Names: nativeActions}
if len(ruleAction.Names) == 0 { if len(ruleAction.Names) == 0 {
continue continue
} }
@ -226,16 +229,19 @@ func formPrincipalKey(principal string, resolver NativeResolver) (string, error)
return key, nil return key, nil
} }
func formNativeActionNames(names []string) []string { func formNativeActionNames(names []string) ([]string, error) {
res := make([]string, 0, len(names)) res := make([]string, 0, len(names))
for i := range names { for i := range names {
trimmed := strings.TrimPrefix(names[i], s3ActionPrefix) action, err := parseActionAsS3Action(names[i])
if trimmed == Wildcard { if err != nil {
return []string{Wildcard} return nil, err
} }
res = append(res, actionToOpMap[trimmed]...) if action == Wildcard {
return []string{Wildcard}, nil
}
res = append(res, actionToOpMap[action]...)
} }
return res return res, nil
} }

View file

@ -2,7 +2,6 @@ package iam
import ( import (
"fmt" "fmt"
"strings"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3" "git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
@ -23,10 +22,18 @@ func ConvertToS3Chain(p Policy, resolver S3Resolver) (*chain.Chain, error) {
status := formStatus(statement) status := formStatus(statement)
action, actionInverted := statement.action() action, actionInverted := statement.action()
ruleAction := chain.Actions{Inverted: actionInverted, Names: formS3ActionNames(action)} s3Actions, err := formS3ActionNames(action)
if err != nil {
return nil, err
}
ruleAction := chain.Actions{Inverted: actionInverted, Names: s3Actions}
resource, resourceInverted := statement.resource() resource, resourceInverted := statement.resource()
ruleResource := chain.Resources{Inverted: resourceInverted, Names: formS3ResourceNamesAndConditions(resource)} s3Resources, err := formS3ResourceNames(resource)
if err != nil {
return nil, err
}
ruleResource := chain.Resources{Inverted: resourceInverted, Names: s3Resources}
groupedConditions, err := convertToS3ChainCondition(statement.Conditions, resolver) groupedConditions, err := convertToS3ChainCondition(statement.Conditions, resolver)
if err != nil { if err != nil {
@ -134,20 +141,33 @@ func formPrincipalOwner(principal string, resolver S3Resolver) (string, error) {
return address, nil return address, nil
} }
func formS3ResourceNamesAndConditions(names []string) []string { func formS3ResourceNames(names []string) ([]string, error) {
res := make([]string, len(names)) res := make([]string, len(names))
for i := range names { for i := range names {
res[i] = strings.TrimPrefix(names[i], s3ResourcePrefix) bkt, obj, err := parseResourceAsS3ARN(names[i])
if err != nil {
return nil, err
} }
return res if bkt == Wildcard {
res[i] = bkt
continue
}
res[i] = bkt + "/" + obj
}
return res, nil
} }
func formS3ActionNames(names []string) []string { func formS3ActionNames(names []string) ([]string, error) {
var err error
res := make([]string, len(names)) res := make([]string, len(names))
for i := range names { for i := range names {
res[i] = strings.TrimPrefix(names[i], s3ActionPrefix) if res[i], err = parseActionAsS3Action(names[i]); err != nil {
return nil, err
}
} }
return res return res, nil
} }

View file

@ -1,6 +1,7 @@
package iam package iam
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
@ -1071,6 +1072,189 @@ func TestComplexS3Conditions(t *testing.T) {
} }
} }
func TestWildcardConverters(t *testing.T) {
policy := `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"*","Resource":"*"}}`
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
_, err = ConvertToS3Chain(p, newMockUserResolver(nil, nil))
require.NoError(t, err)
_, err = ConvertToNativeChain(p, newMockUserResolver(nil, nil))
require.NoError(t, err)
}
func TestActionParsing(t *testing.T) {
for _, tc := range []struct {
action string
expected string
err bool
}{
{
action: "withoutPrefix",
expected: "",
err: true,
},
{
action: "s3:*Object",
expected: "",
err: true,
},
{
action: "*",
expected: "*",
},
{
action: "s3:PutObject",
expected: "PutObject",
},
{
action: "s3:Put*",
expected: "Put*",
},
{
action: "s3:*",
expected: "*",
},
{
action: "s3:",
expected: "",
},
} {
t.Run("", func(t *testing.T) {
actual, err := parseActionAsS3Action(tc.action)
if tc.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.expected, actual)
})
}
}
func TestPrincipalParsing(t *testing.T) {
for _, tc := range []struct {
principal string
expectedAccount string
expectedUser string
err bool
}{
{
principal: "withoutPrefix",
err: true,
},
{
principal: "*",
err: true,
},
{
principal: "arn:aws:iam:::dummy",
err: true,
},
{
principal: "arn:aws:iam::",
err: true,
},
{
principal: "arn:aws:iam:::dummy/test",
err: true,
},
{
principal: "arn:aws:iam:::user/",
err: true,
},
{
principal: "arn:aws:iam:::user/user/",
err: true,
},
{
principal: "arn:aws:iam:::user/name",
expectedUser: "name",
},
{
principal: "arn:aws:iam:::user/path/name",
expectedUser: "name",
},
{
principal: "arn:aws:iam::root:user/path/name",
expectedAccount: "root",
expectedUser: "name",
},
} {
t.Run("", func(t *testing.T) {
account, user, err := parsePrincipalAsIAMUser(tc.principal)
if tc.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.expectedAccount, account)
require.Equal(t, tc.expectedUser, user)
})
}
}
func TestResourceParsing(t *testing.T) {
for _, tc := range []struct {
resource string
expectedBucket string
expectedObject string
err bool
}{
{
resource: "withoutPrefixAnd",
err: true,
},
{
resource: "arn:aws:s3:::*/obj",
err: true,
},
{
resource: "arn:aws:s3:::bkt/*",
expectedBucket: "bkt",
expectedObject: "*",
},
{
resource: "arn:aws:s3:::bkt",
expectedBucket: "bkt",
expectedObject: "*",
},
{
resource: "arn:aws:s3:::bkt/",
expectedBucket: "bkt",
expectedObject: "*",
},
{
resource: "arn:aws:s3:::*",
expectedBucket: "*",
expectedObject: "*",
},
{
resource: "*",
expectedBucket: "*",
expectedObject: "*",
},
} {
t.Run("", func(t *testing.T) {
bkt, obj, err := parseResourceAsS3ARN(tc.resource)
if tc.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.expectedBucket, bkt)
require.Equal(t, tc.expectedObject, obj)
})
}
}
func requireChainRulesMatch(t *testing.T, expected, actual []chain.Rule) { func requireChainRulesMatch(t *testing.T, expected, actual []chain.Rule) {
require.Equal(t, len(expected), len(actual), "length of chain rules differ") require.Equal(t, len(expected), len(actual), "length of chain rules differ")