forked from TrueCloudLab/policy-engine
Compare commits
33 commits
feature/3_
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
839f22e1a3 | ||
|
cf1f091e26 | ||
|
9e66ce59c6 | ||
c960b1b088 | |||
|
8354a074c4 | ||
4a989d6bb7 | |||
0edc002441 | |||
1cdb3e5a4a | |||
af388779a3 | |||
8cc5173d73 | |||
2af381ae81 | |||
8d21ab2d99 | |||
0a28f0a992 | |||
dd0f582fc3 | |||
5f13d91c0d | |||
88c2a476b0 | |||
58386edf58 | |||
06cbfe8691 | |||
c80c99b13e | |||
ed93bb5cc5 | |||
06e9c91014 | |||
b82544b0fe | |||
641a1429ef | |||
02e50307df | |||
3128352693 | |||
ec39d8371a | |||
|
e57d213595 | ||
|
62ea96b82c | ||
1d07331f5d | |||
3b107e9413 | |||
8c673ee4f4 | |||
1375e8f7fd | |||
|
156018bcba |
33 changed files with 2636 additions and 332 deletions
|
@ -16,6 +16,6 @@ jobs:
|
||||||
go-version: '1.21'
|
go-version: '1.21'
|
||||||
|
|
||||||
- name: Run commit format checker
|
- name: Run commit format checker
|
||||||
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v2
|
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v3
|
||||||
with:
|
with:
|
||||||
from: 'origin/${{ github.event.pull_request.base.ref }}'
|
from: 'origin/${{ github.event.pull_request.base.ref }}'
|
||||||
|
|
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -1,2 +1,4 @@
|
||||||
/**/*.pb.go -diff -merge
|
/**/*.pb.go -diff -merge
|
||||||
/**/*.pb.go linguist-generated=true
|
/**/*.pb.go linguist-generated=true
|
||||||
|
/**/*_easyjson.go -diff -merge
|
||||||
|
/**/*_easyjson.go linguist-generated=true
|
14
Makefile
14
Makefile
|
@ -5,6 +5,8 @@ TMP_DIR := .cache
|
||||||
OUTPUT_LINT_DIR ?= $(shell pwd)/bin
|
OUTPUT_LINT_DIR ?= $(shell pwd)/bin
|
||||||
LINT_VERSION ?= 1.55.1
|
LINT_VERSION ?= 1.55.1
|
||||||
LINT_DIR = $(OUTPUT_LINT_DIR)/golangci-lint-$(LINT_VERSION)-v$(TRUECLOUDLAB_LINT_VERSION)
|
LINT_DIR = $(OUTPUT_LINT_DIR)/golangci-lint-$(LINT_VERSION)-v$(TRUECLOUDLAB_LINT_VERSION)
|
||||||
|
EASYJSON_VERSION ?= $(shell go list -f '{{.Version}}' -m github.com/mailru/easyjson)
|
||||||
|
EASYJSON_DIR ?= $(shell pwd)/bin/easyjson-$(EASYJSON_VERSION)
|
||||||
|
|
||||||
# Run all code formatters
|
# Run all code formatters
|
||||||
fmts: fmt imports
|
fmts: fmt imports
|
||||||
|
@ -60,3 +62,15 @@ staticcheck-install:
|
||||||
# Run staticcheck
|
# Run staticcheck
|
||||||
staticcheck-run:
|
staticcheck-run:
|
||||||
@staticcheck ./...
|
@staticcheck ./...
|
||||||
|
|
||||||
|
easyjson-install:
|
||||||
|
@rm -rf $(EASYJSON_DIR)
|
||||||
|
@mkdir -p $(EASYJSON_DIR)
|
||||||
|
@GOBIN=$(EASYJSON_DIR) go install github.com/mailru/easyjson/...@$(EASYJSON_VERSION)
|
||||||
|
|
||||||
|
generate:
|
||||||
|
@if [ ! -d "$(EASYJSON_DIR)" ]; then \
|
||||||
|
make easyjson-install; \
|
||||||
|
fi
|
||||||
|
find ./ -name "_easyjson.go" -exec rm -rf {} \;
|
||||||
|
$(EASYJSON_DIR)/easyjson ./pkg/chain/chain.go
|
20
docs/resource.md
Normal file
20
docs/resource.md
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Resource
|
||||||
|
|
||||||
|
From the point of the access policy engine, a resource is an object to which a request is being performed.
|
||||||
|
This can be an object in a container within a namespace, or all objects in a container,
|
||||||
|
or all containers within the root namespace etc.
|
||||||
|
|
||||||
|
A resource can be viewed from two sides:
|
||||||
|
- As part of a [request](../pkg/resource/resource.go). In this case a resource has a name and properties.
|
||||||
|
- As part of rule [chain](../pkg/chain/chain.go): a resource has just a name.
|
||||||
|
|
||||||
|
## Resource name
|
||||||
|
|
||||||
|
A resource name must have a such format that can be processed by a chain router that matches a request
|
||||||
|
either with local overrides or with rules within policy contract to get if this request is allowed to be performed.
|
||||||
|
The main idea of this format is for the chain router to match by full name (`native:object//cnrID/objID`) or
|
||||||
|
wildcard (`native:object//cnrID/*`).
|
||||||
|
|
||||||
|
Check out formats that are defined in the schema: [native formats](../schema/native/consts.go), [s3 formats](../schema/s3/consts.go).
|
||||||
|
You should validate a resource name using [util](../schema/native/util/validation.go) before instantiating a request or
|
||||||
|
before putting it to either to local override storage or the policy contract storage.
|
26
go.mod
26
go.mod
|
@ -4,25 +4,25 @@ go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958
|
git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958
|
||||||
github.com/mr-tron/base58 v1.2.0
|
github.com/google/uuid v1.3.0
|
||||||
|
github.com/mailru/easyjson v0.7.7
|
||||||
github.com/nspcc-dev/neo-go v0.103.0
|
github.com/nspcc-dev/neo-go v0.103.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
)
|
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
|
||||||
github.com/hashicorp/golang-lru v0.6.0 // indirect
|
|
||||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect
|
|
||||||
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231020160724-c3955f87d1b5 // indirect
|
|
||||||
github.com/nspcc-dev/rfc6979 v0.2.0 // indirect
|
|
||||||
golang.org/x/crypto v0.14.0 // indirect
|
|
||||||
golang.org/x/sync v0.3.0 // indirect
|
|
||||||
golang.org/x/text v0.13.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||||
|
github.com/hashicorp/golang-lru v0.6.0 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
|
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect
|
||||||
|
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231020160724-c3955f87d1b5 // indirect
|
||||||
|
github.com/nspcc-dev/rfc6979 v0.2.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
golang.org/x/crypto v0.14.0 // indirect
|
||||||
|
golang.org/x/sync v0.3.0 // indirect
|
||||||
|
golang.org/x/text v0.13.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -11,6 +11,10 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
|
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
|
||||||
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 h1:n4ZaFCKt1pQJd7PXoMJabZWK9ejjbLOVrkl/lOUmshg=
|
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 h1:n4ZaFCKt1pQJd7PXoMJabZWK9ejjbLOVrkl/lOUmshg=
|
||||||
|
@ -31,6 +35,8 @@ github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1
|
||||||
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
|
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
|
||||||
|
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
|
|
@ -50,12 +50,16 @@ const (
|
||||||
CondArnLike string = "ArnLike"
|
CondArnLike string = "ArnLike"
|
||||||
CondArnNotEquals string = "ArnNotEquals"
|
CondArnNotEquals string = "ArnNotEquals"
|
||||||
CondArnNotLike string = "ArnNotLike"
|
CondArnNotLike string = "ArnNotLike"
|
||||||
|
|
||||||
|
// Custom condition operators.
|
||||||
|
CondSliceContains string = "SliceContains"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
arnIAMPrefix = "arn:aws:iam::"
|
arnIAMPrefix = "arn:aws:iam::"
|
||||||
s3ResourcePrefix = "arn:aws:s3:::"
|
s3ResourcePrefix = "arn:aws:s3:::"
|
||||||
s3ActionPrefix = "s3:"
|
s3ActionPrefix = "s3:"
|
||||||
|
iamActionPrefix = "iam:"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -67,6 +71,9 @@ var (
|
||||||
|
|
||||||
// ErrInvalidActionFormat occurs when action has unknown/unsupported format.
|
// ErrInvalidActionFormat occurs when action has unknown/unsupported format.
|
||||||
ErrInvalidActionFormat = errors.New("invalid action format")
|
ErrInvalidActionFormat = errors.New("invalid action format")
|
||||||
|
|
||||||
|
// ErrActionsNotApplicable occurs when failed to convert any actions.
|
||||||
|
ErrActionsNotApplicable = errors.New("actions not applicable")
|
||||||
)
|
)
|
||||||
|
|
||||||
type formPrincipalConditionFunc func(string) chain.Condition
|
type formPrincipalConditionFunc func(string) chain.Condition
|
||||||
|
@ -190,6 +197,8 @@ func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFuncti
|
||||||
return chain.CondStringLike, noConvertFunction, nil
|
return chain.CondStringLike, noConvertFunction, nil
|
||||||
case op == CondNotIPAddress:
|
case op == CondNotIPAddress:
|
||||||
return chain.CondStringNotLike, noConvertFunction, nil
|
return chain.CondStringNotLike, noConvertFunction, nil
|
||||||
|
case op == CondSliceContains:
|
||||||
|
return chain.CondSliceContains, noConvertFunction, nil
|
||||||
default:
|
default:
|
||||||
return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op)
|
return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op)
|
||||||
}
|
}
|
||||||
|
@ -243,53 +252,38 @@ func parsePrincipalAsIAMUser(principal string) (account string, user string, err
|
||||||
return account, user, nil
|
return account, user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseResourceAsS3ARN(resource string) (bucket string, object string, err error) {
|
func validateResource(resource string) error {
|
||||||
if resource == Wildcard {
|
if resource == Wildcard {
|
||||||
return Wildcard, Wildcard, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(resource, s3ResourcePrefix) {
|
if !strings.HasPrefix(resource, s3ResourcePrefix) && !strings.HasPrefix(resource, arnIAMPrefix) {
|
||||||
return "", "", ErrInvalidResourceFormat
|
return ErrInvalidResourceFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
// iam arn format arn:aws:s3:::<bucket-name>/<object-name>
|
index := strings.IndexByte(resource, Wildcard[0])
|
||||||
s3Resource := strings.TrimPrefix(resource, s3ResourcePrefix)
|
if index != -1 && index != utf8.RuneCountInString(resource)-1 {
|
||||||
sepIndex := strings.Index(s3Resource, "/")
|
return ErrInvalidResourceFormat
|
||||||
if sepIndex < 0 {
|
|
||||||
return s3Resource, Wildcard, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket = s3Resource[:sepIndex]
|
return nil
|
||||||
object = s3Resource[sepIndex+1:]
|
|
||||||
if len(object) == 0 {
|
|
||||||
return bucket, Wildcard, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if bucket == Wildcard && object != Wildcard {
|
|
||||||
return "", "", ErrInvalidResourceFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
return bucket, object, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseActionAsS3Action(action string) (string, error) {
|
func validateAction(action string) error {
|
||||||
if action == Wildcard {
|
if action == Wildcard {
|
||||||
return Wildcard, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(action, s3ActionPrefix) {
|
if !strings.HasPrefix(action, s3ActionPrefix) && !strings.HasPrefix(action, iamActionPrefix) {
|
||||||
return "", ErrInvalidActionFormat
|
return ErrInvalidActionFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
// iam arn format :s3:<action-name>
|
index := strings.IndexByte(action, Wildcard[0])
|
||||||
s3Action := strings.TrimPrefix(action, s3ActionPrefix)
|
if index != -1 && index != utf8.RuneCountInString(action)-1 {
|
||||||
|
return ErrInvalidActionFormat
|
||||||
index := strings.IndexByte(s3Action, Wildcard[0])
|
|
||||||
if index != -1 && index != utf8.RuneCountInString(s3Action)-1 {
|
|
||||||
return "", ErrInvalidActionFormat
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s3Action, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition {
|
func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package iam
|
package iam
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"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"
|
||||||
|
@ -10,28 +10,61 @@ import (
|
||||||
|
|
||||||
const PropertyKeyFilePath = "FilePath"
|
const PropertyKeyFilePath = "FilePath"
|
||||||
|
|
||||||
// ErrActionsNotApplicable occurs when failed to convert any actions.
|
var supportedActionToNativeOpMap = map[string][]string{
|
||||||
var ErrActionsNotApplicable = errors.New("actions not applicable")
|
supportedS3NativeActionDeleteObject: {native.MethodDeleteObject, native.MethodHeadObject},
|
||||||
|
supportedS3NativeActionHeadObject: {native.MethodHeadObject},
|
||||||
|
supportedS3NativeActionGetObject: {native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
||||||
|
supportedS3NativeActionPutObject: {native.MethodPutObject},
|
||||||
|
supportedS3NativeActionListBucket: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
||||||
|
|
||||||
var actionToOpMap = map[string][]string{
|
supportedS3NativeActionCreateBucket: {native.MethodPutContainer},
|
||||||
supportedS3ActionDeleteObject: {native.MethodDeleteObject},
|
supportedS3NativeActionDeleteBucket: {native.MethodDeleteContainer},
|
||||||
supportedS3ActionGetObject: {native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
supportedS3NativeActionListAllMyBucket: {native.MethodListContainers},
|
||||||
supportedS3ActionHeadObject: {native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
supportedS3NativeActionPutBucketACL: {native.MethodSetContainerEACL},
|
||||||
supportedS3ActionPutObject: {native.MethodPutObject},
|
supportedS3NativeActionGetBucketACL: {native.MethodGetContainerEACL},
|
||||||
supportedS3ActionListBucket: {native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
}
|
||||||
|
|
||||||
|
var containerNativeOperations = map[string]struct{}{
|
||||||
|
native.MethodPutContainer: {},
|
||||||
|
native.MethodDeleteContainer: {},
|
||||||
|
native.MethodGetContainer: {},
|
||||||
|
native.MethodListContainers: {},
|
||||||
|
native.MethodSetContainerEACL: {},
|
||||||
|
native.MethodGetContainerEACL: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
var objectNativeOperations = map[string]struct{}{
|
||||||
|
native.MethodGetObject: {},
|
||||||
|
native.MethodPutObject: {},
|
||||||
|
native.MethodHeadObject: {},
|
||||||
|
native.MethodDeleteObject: {},
|
||||||
|
native.MethodSearchObject: {},
|
||||||
|
native.MethodRangeObject: {},
|
||||||
|
native.MethodHashObject: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
supportedS3ActionDeleteObject = "DeleteObject"
|
supportedS3NativeActionDeleteObject = "s3:DeleteObject"
|
||||||
supportedS3ActionGetObject = "GetObject"
|
supportedS3NativeActionGetObject = "s3:GetObject"
|
||||||
supportedS3ActionHeadObject = "HeadObject"
|
supportedS3NativeActionHeadObject = "s3:HeadObject"
|
||||||
supportedS3ActionPutObject = "PutObject"
|
supportedS3NativeActionPutObject = "s3:PutObject"
|
||||||
supportedS3ActionListBucket = "ListBucket"
|
supportedS3NativeActionListBucket = "s3:ListBucket"
|
||||||
|
|
||||||
|
supportedS3NativeActionCreateBucket = "s3:CreateBucket"
|
||||||
|
supportedS3NativeActionDeleteBucket = "s3:DeleteBucket"
|
||||||
|
supportedS3NativeActionListAllMyBucket = "s3:ListAllMyBuckets"
|
||||||
|
supportedS3NativeActionPutBucketACL = "s3:PutBucketAcl"
|
||||||
|
supportedS3NativeActionGetBucketACL = "s3:GetBucketAcl"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NativeResolver interface {
|
type NativeResolver interface {
|
||||||
GetUserKey(account, name string) (string, error)
|
GetUserKey(account, name string) (string, error)
|
||||||
GetBucketCID(bucket string) (string, error)
|
GetBucketInfo(bucket string) (*BucketInfo, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BucketInfo struct {
|
||||||
|
Namespace string
|
||||||
|
Container string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, error) {
|
func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, error) {
|
||||||
|
@ -55,7 +88,7 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro
|
||||||
}
|
}
|
||||||
|
|
||||||
resource, resourceInverted := statement.resource()
|
resource, resourceInverted := statement.resource()
|
||||||
groupedResources, err := formNativeResourceNamesAndConditions(resource, resolver)
|
groupedResources, err := formNativeResourceNamesAndConditions(resource, resolver, getActionTypes(nativeActions))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -74,7 +107,12 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro
|
||||||
for _, groupedResource := range groupedResources {
|
for _, groupedResource := range groupedResources {
|
||||||
for _, principal := range principals {
|
for _, principal := range principals {
|
||||||
for _, conditions := range splitConditions {
|
for _, conditions := range splitConditions {
|
||||||
ruleConditions := append([]chain.Condition{principalCondFn(principal)}, groupedResource.Conditions...)
|
var principalCondition []chain.Condition
|
||||||
|
if principal != Wildcard {
|
||||||
|
principalCondition = []chain.Condition{principalCondFn(principal)}
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleConditions := append(principalCondition, groupedResource.Conditions...)
|
||||||
|
|
||||||
r := chain.Rule{
|
r := chain.Rule{
|
||||||
Status: status,
|
Status: status,
|
||||||
|
@ -98,6 +136,23 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro
|
||||||
return &engineChain, nil
|
return &engineChain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getActionTypes(nativeActions []string) ActionTypes {
|
||||||
|
var res ActionTypes
|
||||||
|
for _, action := range nativeActions {
|
||||||
|
if res.Object && res.Container {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
_, isObj := objectNativeOperations[action]
|
||||||
|
_, isCnr := containerNativeOperations[action]
|
||||||
|
|
||||||
|
res.Object = isObj || action == Wildcard
|
||||||
|
res.Container = isCnr || action == Wildcard
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
func getNativePrincipalsAndConditionFunc(statement Statement, resolver NativeResolver) ([]string, formPrincipalConditionFunc, error) {
|
func getNativePrincipalsAndConditionFunc(statement Statement, resolver NativeResolver) ([]string, formPrincipalConditionFunc, error) {
|
||||||
var principals []string
|
var principals []string
|
||||||
var op chain.ConditionType
|
var op chain.ConditionType
|
||||||
|
@ -155,35 +210,67 @@ type GroupedResources struct {
|
||||||
Conditions []chain.Condition
|
Conditions []chain.Condition
|
||||||
}
|
}
|
||||||
|
|
||||||
func formNativeResourceNamesAndConditions(names []string, resolver NativeResolver) ([]GroupedResources, error) {
|
type ActionTypes struct {
|
||||||
|
Object bool
|
||||||
|
Container bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func formNativeResourceNamesAndConditions(names []string, resolver NativeResolver, actionTypes ActionTypes) ([]GroupedResources, error) {
|
||||||
|
if !actionTypes.Object && !actionTypes.Container {
|
||||||
|
return nil, ErrActionsNotApplicable
|
||||||
|
}
|
||||||
|
|
||||||
res := make([]GroupedResources, 0, len(names))
|
res := make([]GroupedResources, 0, len(names))
|
||||||
|
|
||||||
var combined []string
|
var combined []string
|
||||||
|
|
||||||
for i := range names {
|
for _, resource := range names {
|
||||||
bkt, obj, err := parseResourceAsS3ARN(names[i])
|
if err := validateResource(resource); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if bkt == Wildcard {
|
if resource == Wildcard {
|
||||||
res = res[:0]
|
res = res[:0]
|
||||||
return append(res, GroupedResources{Names: []string{native.ResourceFormatAllObjects}}), nil
|
return append(res, formWildcardNativeResource(actionTypes)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cnrID, err := resolver.GetBucketCID(bkt)
|
if !strings.HasPrefix(resource, s3ResourcePrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var bkt, obj string
|
||||||
|
s3Resource := strings.TrimPrefix(resource, s3ResourcePrefix)
|
||||||
|
if s3Resource == Wildcard {
|
||||||
|
res = res[:0]
|
||||||
|
return append(res, formWildcardNativeResource(actionTypes)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if sepIndex := strings.Index(s3Resource, "/"); sepIndex < 0 {
|
||||||
|
bkt = s3Resource
|
||||||
|
} else {
|
||||||
|
bkt = s3Resource[:sepIndex]
|
||||||
|
obj = s3Resource[sepIndex+1:]
|
||||||
|
if len(obj) == 0 {
|
||||||
|
obj = Wildcard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bktInfo, err := resolver.GetBucketInfo(bkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
resource := fmt.Sprintf(native.ResourceFormatRootContainerObjects, cnrID)
|
|
||||||
|
|
||||||
if obj == Wildcard {
|
if obj == Wildcard && actionTypes.Object { // this corresponds to arn:aws:s3:::BUCKET/ or arn:aws:s3:::BUCKET/*
|
||||||
combined = append(combined, resource)
|
combined = append(combined, fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if obj == "" && actionTypes.Container { // this corresponds to arn:aws:s3:::BUCKET
|
||||||
|
combined = append(combined, fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
res = append(res, GroupedResources{
|
res = append(res, GroupedResources{
|
||||||
Names: []string{resource},
|
Names: []string{fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)},
|
||||||
Conditions: []chain.Condition{
|
Conditions: []chain.Condition{
|
||||||
{
|
{
|
||||||
Op: chain.CondStringLike,
|
Op: chain.CondStringLike,
|
||||||
|
@ -202,6 +289,18 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formWildcardNativeResource(actionTypes ActionTypes) GroupedResources {
|
||||||
|
groupedNames := make([]string, 0, 2)
|
||||||
|
if actionTypes.Object {
|
||||||
|
groupedNames = append(groupedNames, native.ResourceFormatAllObjects)
|
||||||
|
}
|
||||||
|
if actionTypes.Container {
|
||||||
|
groupedNames = append(groupedNames, native.ResourceFormatAllContainers)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GroupedResources{Names: groupedNames}
|
||||||
|
}
|
||||||
|
|
||||||
func formNativePrincipal(principal []string, resolver NativeResolver) ([]string, error) {
|
func formNativePrincipal(principal []string, resolver NativeResolver) ([]string, error) {
|
||||||
res := make([]string, len(principal))
|
res := make([]string, len(principal))
|
||||||
|
|
||||||
|
@ -232,15 +331,24 @@ func formPrincipalKey(principal string, resolver NativeResolver) (string, error)
|
||||||
func formNativeActionNames(names []string) ([]string, error) {
|
func formNativeActionNames(names []string) ([]string, error) {
|
||||||
res := make([]string, 0, len(names))
|
res := make([]string, 0, len(names))
|
||||||
|
|
||||||
for i := range names {
|
for _, action := range names {
|
||||||
action, err := parseActionAsS3Action(names[i])
|
if err := validateAction(action); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if action == Wildcard {
|
if action == Wildcard {
|
||||||
return []string{Wildcard}, nil
|
return []string{Wildcard}, nil
|
||||||
}
|
}
|
||||||
res = append(res, actionToOpMap[action]...)
|
|
||||||
|
if !strings.HasPrefix(action, s3ActionPrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimPrefix(action, s3ActionPrefix) == Wildcard {
|
||||||
|
return []string{Wildcard}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, supportedActionToNativeOpMap[action]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
|
|
|
@ -7,6 +7,76 @@ import (
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
|
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var specialActionToS3OpMap = map[string][]string{
|
||||||
|
specialS3ActionsListAllMyBuckets: {"s3:ListBuckets"},
|
||||||
|
specialS3ActionsListBucket: {"s3:HeadBucket", "s3:GetBucketLocation", "s3:ListObjectsV1", "s3:ListObjectsV2"},
|
||||||
|
specialS3ActionsListBucketVersions: {"s3:ListBucketObjectVersions"},
|
||||||
|
specialS3ActionsListBucketMultipartUploads: {"s3:ListMultipartUploads"},
|
||||||
|
specialS3ActionsGetBucketObjectLockConfiguration: {"s3:GetBucketObjectLockConfig"},
|
||||||
|
specialS3ActionsGetEncryptionConfiguration: {"s3:GetBucketEncryption"},
|
||||||
|
specialS3ActionsGetLifecycleConfiguration: {"s3:GetBucketLifecycle"},
|
||||||
|
specialS3ActionsGetBucketACL: {"s3:GetBucketACL"},
|
||||||
|
specialS3ActionsGetBucketCORS: {"s3:GetBucketCors"},
|
||||||
|
specialS3ActionsPutBucketTagging: {"s3:PutBucketTagging", "s3:DeleteBucketTagging"},
|
||||||
|
specialS3ActionsPutBucketObjectLockConfiguration: {"s3:PutBucketObjectLockConfig"},
|
||||||
|
specialS3ActionsPutEncryptionConfiguration: {"s3:PutBucketEncryption", "s3:DeleteBucketEncryption"},
|
||||||
|
specialS3ActionsPutLifecycleConfiguration: {"s3:PutBucketLifecycle", "s3:DeleteBucketLifecycle"},
|
||||||
|
specialS3ActionsPutBucketACL: {"s3:PutBucketACL"},
|
||||||
|
specialS3ActionsPutBucketCORS: {"s3:PutBucketCors", "s3:DeleteBucketCors"},
|
||||||
|
specialS3ActionsDeleteBucketCORS: {"s3:DeleteBucketCors"},
|
||||||
|
|
||||||
|
specialS3ActionsListMultipartUploadParts: {"s3:ListParts"},
|
||||||
|
specialS3ActionsGetObjectACL: {"s3:GetObjectACL"},
|
||||||
|
specialS3ActionsGetObject: {"s3:GetObject", "s3:HeadObject"},
|
||||||
|
specialS3ActionsGetObjectVersion: {"s3:GetObject", "s3:HeadObject"},
|
||||||
|
specialS3ActionsGetObjectVersionACL: {"s3:GetObjectACL"},
|
||||||
|
specialS3ActionsGetObjectVersionAttributes: {"s3:GetObjectAttributes"},
|
||||||
|
specialS3ActionsGetObjectVersionTagging: {"s3:GetObjectTagging"},
|
||||||
|
specialS3ActionsPutObjectACL: {"s3:PutObjectACL"},
|
||||||
|
specialS3ActionsPutObjectVersionACL: {"s3:PutObjectACL"},
|
||||||
|
specialS3ActionsPutObjectVersionTagging: {"s3:PutObjectTagging"},
|
||||||
|
specialS3ActionsPutObject: {
|
||||||
|
"s3:PutObject", "s3:PostObject", "s3:CopyObject",
|
||||||
|
"s3:UploadPart", "s3:UploadPartCopy", "s3:CreateMultipartUpload", "s3:CompleteMultipartUpload",
|
||||||
|
},
|
||||||
|
specialS3ActionsDeleteObjectVersionTagging: {"s3:DeleteObjectTagging"},
|
||||||
|
specialS3ActionsDeleteObject: {"s3:DeleteObject", "s3:DeleteMultipleObjects"},
|
||||||
|
specialS3ActionsDeleteObjectVersion: {"s3:DeleteObject", "s3:DeleteMultipleObjects"},
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
specialS3ActionsListAllMyBuckets = "s3:ListAllMyBuckets"
|
||||||
|
specialS3ActionsListBucket = "s3:ListBucket"
|
||||||
|
specialS3ActionsListBucketVersions = "s3:ListBucketVersions"
|
||||||
|
specialS3ActionsListBucketMultipartUploads = "s3:ListBucketMultipartUploads"
|
||||||
|
specialS3ActionsGetBucketObjectLockConfiguration = "s3:GetBucketObjectLockConfiguration"
|
||||||
|
specialS3ActionsGetEncryptionConfiguration = "s3:GetEncryptionConfiguration"
|
||||||
|
specialS3ActionsGetLifecycleConfiguration = "s3:GetLifecycleConfiguration"
|
||||||
|
specialS3ActionsGetBucketACL = "s3:GetBucketAcl"
|
||||||
|
specialS3ActionsGetBucketCORS = "s3:GetBucketCORS"
|
||||||
|
specialS3ActionsPutBucketTagging = "s3:PutBucketTagging"
|
||||||
|
specialS3ActionsPutBucketObjectLockConfiguration = "s3:PutBucketObjectLockConfiguration"
|
||||||
|
specialS3ActionsPutEncryptionConfiguration = "s3:PutEncryptionConfiguration"
|
||||||
|
specialS3ActionsPutLifecycleConfiguration = "s3:PutLifecycleConfiguration"
|
||||||
|
specialS3ActionsPutBucketACL = "s3:PutBucketAcl"
|
||||||
|
specialS3ActionsPutBucketCORS = "s3:PutBucketCORS"
|
||||||
|
specialS3ActionsDeleteBucketCORS = "s3:DeleteBucketCORS"
|
||||||
|
specialS3ActionsListMultipartUploadParts = "s3:ListMultipartUploadParts"
|
||||||
|
specialS3ActionsGetObjectACL = "s3:GetObjectAcl"
|
||||||
|
specialS3ActionsGetObject = "s3:GetObject"
|
||||||
|
specialS3ActionsGetObjectVersion = "s3:GetObjectVersion"
|
||||||
|
specialS3ActionsGetObjectVersionACL = "s3:GetObjectVersionAcl"
|
||||||
|
specialS3ActionsGetObjectVersionAttributes = "s3:GetObjectVersionAttributes"
|
||||||
|
specialS3ActionsGetObjectVersionTagging = "s3:GetObjectVersionTagging"
|
||||||
|
specialS3ActionsPutObjectACL = "s3:PutObjectAcl"
|
||||||
|
specialS3ActionsPutObjectVersionACL = "s3:PutObjectVersionAcl"
|
||||||
|
specialS3ActionsPutObjectVersionTagging = "s3:PutObjectVersionTagging"
|
||||||
|
specialS3ActionsPutObject = "s3:PutObject"
|
||||||
|
specialS3ActionsDeleteObjectVersionTagging = "s3:DeleteObjectVersionTagging"
|
||||||
|
specialS3ActionsDeleteObject = "s3:DeleteObject"
|
||||||
|
specialS3ActionsDeleteObjectVersion = "s3:DeleteObjectVersion"
|
||||||
|
)
|
||||||
|
|
||||||
type S3Resolver interface {
|
type S3Resolver interface {
|
||||||
GetUserAddress(account, user string) (string, error)
|
GetUserAddress(account, user string) (string, error)
|
||||||
}
|
}
|
||||||
|
@ -21,19 +91,21 @@ func ConvertToS3Chain(p Policy, resolver S3Resolver) (*chain.Chain, error) {
|
||||||
for _, statement := range p.Statement {
|
for _, statement := range p.Statement {
|
||||||
status := formStatus(statement)
|
status := formStatus(statement)
|
||||||
|
|
||||||
action, actionInverted := statement.action()
|
actions, actionInverted := statement.action()
|
||||||
s3Actions, err := formS3ActionNames(action)
|
s3Actions, err := formS3ActionNames(actions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ruleAction := chain.Actions{Inverted: actionInverted, Names: s3Actions}
|
ruleAction := chain.Actions{Inverted: actionInverted, Names: s3Actions}
|
||||||
|
if len(ruleAction.Names) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
resource, resourceInverted := statement.resource()
|
resources, resourceInverted := statement.resource()
|
||||||
s3Resources, err := formS3ResourceNames(resource)
|
if err := validateS3ResourceNames(resources); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ruleResource := chain.Resources{Inverted: resourceInverted, Names: s3Resources}
|
ruleResource := chain.Resources{Inverted: resourceInverted, Names: resources}
|
||||||
|
|
||||||
groupedConditions, err := convertToS3ChainCondition(statement.Conditions, resolver)
|
groupedConditions, err := convertToS3ChainCondition(statement.Conditions, resolver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -48,17 +120,26 @@ func ConvertToS3Chain(p Policy, resolver S3Resolver) (*chain.Chain, error) {
|
||||||
|
|
||||||
for _, principal := range principals {
|
for _, principal := range principals {
|
||||||
for _, conditions := range splitConditions {
|
for _, conditions := range splitConditions {
|
||||||
|
var principalCondition []chain.Condition
|
||||||
|
if principal != Wildcard {
|
||||||
|
principalCondition = []chain.Condition{principalCondFn(principal)}
|
||||||
|
}
|
||||||
|
|
||||||
r := chain.Rule{
|
r := chain.Rule{
|
||||||
Status: status,
|
Status: status,
|
||||||
Actions: ruleAction,
|
Actions: ruleAction,
|
||||||
Resources: ruleResource,
|
Resources: ruleResource,
|
||||||
Condition: append([]chain.Condition{principalCondFn(principal)}, conditions...),
|
Condition: append(principalCondition, conditions...),
|
||||||
}
|
}
|
||||||
engineChain.Rules = append(engineChain.Rules, r)
|
engineChain.Rules = append(engineChain.Rules, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(engineChain.Rules) == 0 {
|
||||||
|
return nil, ErrActionsNotApplicable
|
||||||
|
}
|
||||||
|
|
||||||
return &engineChain, nil
|
return &engineChain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,32 +222,33 @@ func formPrincipalOwner(principal string, resolver S3Resolver) (string, error) {
|
||||||
return address, nil
|
return address, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func formS3ResourceNames(names []string) ([]string, error) {
|
func validateS3ResourceNames(names []string) error {
|
||||||
res := make([]string, len(names))
|
|
||||||
for i := range names {
|
for i := range names {
|
||||||
bkt, obj, err := parseResourceAsS3ARN(names[i])
|
if err := validateResource(names[i]); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if bkt == Wildcard {
|
|
||||||
res[i] = bkt
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
res[i] = bkt + "/" + obj
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func formS3ActionNames(names []string) ([]string, error) {
|
func formS3ActionNames(names []string) ([]string, error) {
|
||||||
var err error
|
res := make([]string, 0, len(names))
|
||||||
res := make([]string, len(names))
|
|
||||||
for i := range names {
|
for _, action := range names {
|
||||||
if res[i], err = parseActionAsS3Action(names[i]); err != nil {
|
if err := validateAction(action); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if action == Wildcard {
|
||||||
|
return []string{Wildcard}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if actions, ok := specialActionToS3OpMap[action]; ok {
|
||||||
|
res = append(res, actions...)
|
||||||
|
} else {
|
||||||
|
res = append(res, action)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
|
|
|
@ -17,22 +17,23 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockUserResolver struct {
|
type mockUserResolver struct {
|
||||||
users map[string]string
|
users map[string]string
|
||||||
buckets map[string]string
|
containers map[string]string
|
||||||
|
namespace string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMockUserResolver(accountUsers []string, buckets []string) *mockUserResolver {
|
func newMockUserResolver(accountUsers []string, buckets []string, namespace string) *mockUserResolver {
|
||||||
userMap := make(map[string]string, len(accountUsers))
|
userMap := make(map[string]string, len(accountUsers))
|
||||||
for _, user := range accountUsers {
|
for _, user := range accountUsers {
|
||||||
userMap[user] = user + "/resolvedValue"
|
userMap[user] = user + "/resolvedValue"
|
||||||
}
|
}
|
||||||
|
|
||||||
bucketMap := make(map[string]string, len(buckets))
|
containerMap := make(map[string]string, len(buckets))
|
||||||
for _, bkt := range buckets {
|
for _, bkt := range buckets {
|
||||||
bucketMap[bkt] = bkt + "/resolvedValues"
|
containerMap[bkt] = bkt + "/resolvedValues"
|
||||||
}
|
}
|
||||||
|
|
||||||
return &mockUserResolver{users: userMap, buckets: bucketMap}
|
return &mockUserResolver{users: userMap, containers: containerMap, namespace: namespace}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockUserResolver) GetUserAddress(account, user string) (string, error) {
|
func (m *mockUserResolver) GetUserAddress(account, user string) (string, error) {
|
||||||
|
@ -53,13 +54,13 @@ func (m *mockUserResolver) GetUserKey(account, user string) (string, error) {
|
||||||
return key, nil
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockUserResolver) GetBucketCID(bkt string) (string, error) {
|
func (m *mockUserResolver) GetBucketInfo(bkt string) (*BucketInfo, error) {
|
||||||
cnrID, ok := m.buckets[bkt]
|
cnr, ok := m.containers[bkt]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", errors.New("not found")
|
return nil, errors.New("not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
return cnrID, nil
|
return &BucketInfo{Container: cnr, Namespace: m.namespace}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConverters(t *testing.T) {
|
func TestConverters(t *testing.T) {
|
||||||
|
@ -69,10 +70,11 @@ func TestConverters(t *testing.T) {
|
||||||
principal := "arn:aws:iam::" + namespace + ":user/" + userName
|
principal := "arn:aws:iam::" + namespace + ":user/" + userName
|
||||||
bktName := "DOC-EXAMPLE-BUCKET"
|
bktName := "DOC-EXAMPLE-BUCKET"
|
||||||
objName := "object-name"
|
objName := "object-name"
|
||||||
resource := bktName + "/*"
|
resource := fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName)
|
||||||
action := "PutObject"
|
s3GetObjectAction := "s3:GetObject"
|
||||||
|
s3HeadObjectAction := "s3:HeadObject"
|
||||||
|
|
||||||
mockResolver := newMockUserResolver([]string{user}, []string{bktName})
|
mockResolver := newMockUserResolver([]string{user}, []string{bktName}, namespace)
|
||||||
|
|
||||||
t.Run("valid policy", func(t *testing.T) {
|
t.Run("valid policy", func(t *testing.T) {
|
||||||
p := Policy{
|
p := Policy{
|
||||||
|
@ -82,8 +84,8 @@ func TestConverters(t *testing.T) {
|
||||||
AWSPrincipalType: {principal},
|
AWSPrincipalType: {principal},
|
||||||
},
|
},
|
||||||
Effect: AllowEffect,
|
Effect: AllowEffect,
|
||||||
Action: []string{"s3:PutObject"},
|
Action: []string{s3GetObjectAction},
|
||||||
Resource: []string{"arn:aws:s3:::" + resource},
|
Resource: []string{resource},
|
||||||
Conditions: map[string]Condition{
|
Conditions: map[string]Condition{
|
||||||
CondStringEquals: {
|
CondStringEquals: {
|
||||||
"s3:RequestObjectTag/Department": {"Finance"},
|
"s3:RequestObjectTag/Department": {"Finance"},
|
||||||
|
@ -95,7 +97,7 @@ func TestConverters(t *testing.T) {
|
||||||
expected := &chain.Chain{Rules: []chain.Rule{
|
expected := &chain.Chain{Rules: []chain.Rule{
|
||||||
{
|
{
|
||||||
Status: chain.Allow,
|
Status: chain.Allow,
|
||||||
Actions: chain.Actions{Names: []string{action}},
|
Actions: chain.Actions{Names: []string{s3GetObjectAction, s3HeadObjectAction}},
|
||||||
Resources: chain.Resources{Names: []string{resource}},
|
Resources: chain.Resources{Names: []string{resource}},
|
||||||
Condition: []chain.Condition{
|
Condition: []chain.Condition{
|
||||||
{
|
{
|
||||||
|
@ -128,15 +130,15 @@ func TestConverters(t *testing.T) {
|
||||||
},
|
},
|
||||||
Effect: AllowEffect,
|
Effect: AllowEffect,
|
||||||
Action: []string{"s3:PutObject"},
|
Action: []string{"s3:PutObject"},
|
||||||
Resource: []string{"arn:aws:s3:::" + resource},
|
Resource: []string{resource},
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := &chain.Chain{Rules: []chain.Rule{
|
expected := &chain.Chain{Rules: []chain.Rule{
|
||||||
{
|
{
|
||||||
Status: chain.Allow,
|
Status: chain.Allow,
|
||||||
Actions: chain.Actions{Names: []string{action}},
|
Actions: chain.Actions{Names: []string{native.MethodPutObject}},
|
||||||
Resources: chain.Resources{Names: []string{fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName])}},
|
Resources: chain.Resources{Names: []string{fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName])}},
|
||||||
Condition: []chain.Condition{
|
Condition: []chain.Condition{
|
||||||
{
|
{
|
||||||
Op: chain.CondStringEquals,
|
Op: chain.CondStringEquals,
|
||||||
|
@ -161,15 +163,15 @@ func TestConverters(t *testing.T) {
|
||||||
AWSPrincipalType: {principal},
|
AWSPrincipalType: {principal},
|
||||||
},
|
},
|
||||||
Effect: DenyEffect,
|
Effect: DenyEffect,
|
||||||
NotAction: []string{"s3:PutObject"},
|
NotAction: []string{s3GetObjectAction},
|
||||||
NotResource: []string{"arn:aws:s3:::" + resource},
|
NotResource: []string{resource},
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := &chain.Chain{Rules: []chain.Rule{
|
expected := &chain.Chain{Rules: []chain.Rule{
|
||||||
{
|
{
|
||||||
Status: chain.AccessDenied,
|
Status: chain.AccessDenied,
|
||||||
Actions: chain.Actions{Inverted: true, Names: []string{action}},
|
Actions: chain.Actions{Inverted: true, Names: []string{s3GetObjectAction, s3HeadObjectAction}},
|
||||||
Resources: chain.Resources{Inverted: true, Names: []string{resource}},
|
Resources: chain.Resources{Inverted: true, Names: []string{resource}},
|
||||||
Condition: []chain.Condition{
|
Condition: []chain.Condition{
|
||||||
{
|
{
|
||||||
|
@ -187,25 +189,28 @@ func TestConverters(t *testing.T) {
|
||||||
require.Equal(t, expected, s3Chain)
|
require.Equal(t, expected, s3Chain)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("valid policy map get action", func(t *testing.T) {
|
t.Run("valid native policy map action", func(t *testing.T) {
|
||||||
p := Policy{
|
p := Policy{
|
||||||
Version: "2012-10-17",
|
Version: "2012-10-17",
|
||||||
Statement: []Statement{{
|
Statement: []Statement{{
|
||||||
Principal: map[PrincipalType][]string{
|
Principal: map[PrincipalType][]string{
|
||||||
AWSPrincipalType: {principal},
|
AWSPrincipalType: {principal},
|
||||||
},
|
},
|
||||||
Effect: DenyEffect,
|
Effect: DenyEffect,
|
||||||
NotAction: []string{"s3:GetObject"},
|
Action: []string{"s3:DeleteObject", "s3:DeleteBucket"},
|
||||||
NotResource: []string{"arn:aws:s3:::" + bktName + "/" + objName},
|
Resource: []string{
|
||||||
|
fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName, objName),
|
||||||
|
fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName),
|
||||||
|
},
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := &chain.Chain{Rules: []chain.Rule{
|
expected := &chain.Chain{Rules: []chain.Rule{
|
||||||
{
|
{
|
||||||
Status: chain.AccessDenied,
|
Status: chain.AccessDenied,
|
||||||
Actions: chain.Actions{Inverted: true, Names: actionToOpMap["GetObject"]},
|
Actions: chain.Actions{Names: []string{native.MethodDeleteObject, native.MethodHeadObject, native.MethodDeleteContainer}},
|
||||||
Resources: chain.Resources{Inverted: true, Names: []string{
|
Resources: chain.Resources{Names: []string{
|
||||||
fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName]),
|
fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName]),
|
||||||
}},
|
}},
|
||||||
Condition: []chain.Condition{
|
Condition: []chain.Condition{
|
||||||
{
|
{
|
||||||
|
@ -222,6 +227,19 @@ func TestConverters(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Status: chain.AccessDenied,
|
||||||
|
Actions: chain.Actions{Names: []string{native.MethodDeleteObject, native.MethodHeadObject, native.MethodDeleteContainer}},
|
||||||
|
Resources: chain.Resources{Names: []string{
|
||||||
|
fmt.Sprintf(native.ResourceFormatNamespaceContainer, namespace, mockResolver.containers[bktName]),
|
||||||
|
}},
|
||||||
|
Condition: []chain.Condition{{
|
||||||
|
Op: chain.CondStringEquals,
|
||||||
|
Object: chain.ObjectRequest,
|
||||||
|
Key: native.PropertyKeyActorPublicKey,
|
||||||
|
Value: mockResolver.users[user],
|
||||||
|
}},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
nativeChain, err := ConvertToNativeChain(p, mockResolver)
|
nativeChain, err := ConvertToNativeChain(p, mockResolver)
|
||||||
|
@ -278,6 +296,65 @@ func TestConverters(t *testing.T) {
|
||||||
_, err := ConvertToNativeChain(p, mockResolver)
|
_, err := ConvertToNativeChain(p, mockResolver)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("invalid policy (missing s3 actions)", func(t *testing.T) {
|
||||||
|
p := Policy{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: []Statement{{
|
||||||
|
Principal: map[PrincipalType][]string{
|
||||||
|
AWSPrincipalType: {principal},
|
||||||
|
},
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Resource: []string{"arn:aws:s3:::" + resource},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ConvertToS3Chain(p, mockResolver)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid mixed iam/s3 actions", func(t *testing.T) {
|
||||||
|
p := Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Principal: map[PrincipalType][]string{AWSPrincipalType: {principal}},
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:DeleteObject", "iam:*"},
|
||||||
|
Resource: []string{"*"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
s3Expected := &chain.Chain{Rules: []chain.Rule{{
|
||||||
|
Status: chain.Allow,
|
||||||
|
Actions: chain.Actions{Names: []string{"s3:DeleteObject", "s3:DeleteMultipleObjects", "iam:*"}},
|
||||||
|
Resources: chain.Resources{Names: []string{"*"}},
|
||||||
|
Condition: []chain.Condition{{
|
||||||
|
Op: chain.CondStringEquals,
|
||||||
|
Object: chain.ObjectRequest,
|
||||||
|
Key: s3.PropertyKeyOwner,
|
||||||
|
Value: mockResolver.users[user],
|
||||||
|
}},
|
||||||
|
}}}
|
||||||
|
|
||||||
|
s3Chain, err := ConvertToS3Chain(p, mockResolver)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, s3Expected, s3Chain)
|
||||||
|
|
||||||
|
nativeExpected := &chain.Chain{Rules: []chain.Rule{{
|
||||||
|
Status: chain.Allow,
|
||||||
|
Actions: chain.Actions{Names: []string{native.MethodDeleteObject, native.MethodHeadObject}},
|
||||||
|
Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects}},
|
||||||
|
Condition: []chain.Condition{{
|
||||||
|
Op: chain.CondStringEquals,
|
||||||
|
Object: chain.ObjectRequest,
|
||||||
|
Key: native.PropertyKeyActorPublicKey,
|
||||||
|
Value: mockResolver.users[user],
|
||||||
|
}},
|
||||||
|
}}}
|
||||||
|
|
||||||
|
nativeChain, err := ConvertToNativeChain(p, mockResolver)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, nativeExpected, nativeChain)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertToChainCondition(t *testing.T) {
|
func TestConvertToChainCondition(t *testing.T) {
|
||||||
|
@ -544,10 +621,10 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
key1, key2 := "key1", "key2"
|
key1, key2 := "key1", "key2"
|
||||||
val0, val1, val2 := "val0", "val1", "val2"
|
val0, val1, val2 := "val0", "val1", "val2"
|
||||||
|
|
||||||
mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3})
|
mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3}, "")
|
||||||
nativeResource1 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName1])
|
nativeResource1 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName1])
|
||||||
nativeResource2 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName2])
|
nativeResource2 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName2])
|
||||||
nativeResource3 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName3])
|
nativeResource3 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName3])
|
||||||
|
|
||||||
p := Policy{
|
p := Policy{
|
||||||
Version: "2012-10-17",
|
Version: "2012-10-17",
|
||||||
|
@ -566,7 +643,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedStatus := chain.AccessDenied
|
expectedStatus := chain.AccessDenied
|
||||||
expectedActions := chain.Actions{Names: actionToOpMap[action]}
|
expectedActions := chain.Actions{Names: supportedActionToNativeOpMap["s3:"+action]}
|
||||||
expectedResource1 := chain.Resources{Names: []string{nativeResource1}}
|
expectedResource1 := chain.Resources{Names: []string{nativeResource1}}
|
||||||
expectedResource23 := chain.Resources{Names: []string{nativeResource2, nativeResource3}}
|
expectedResource23 := chain.Resources{Names: []string{nativeResource2, nativeResource3}}
|
||||||
|
|
||||||
|
@ -683,7 +760,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket resource1, all conditions matched",
|
name: "bucket resource1, all conditions matched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"),
|
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName2], "some-oid"),
|
||||||
resourceMap: map[string]string{
|
resourceMap: map[string]string{
|
||||||
PropertyKeyFilePath: "any-object-name",
|
PropertyKeyFilePath: "any-object-name",
|
||||||
},
|
},
|
||||||
|
@ -697,7 +774,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket resource3, all conditions matched",
|
name: "bucket resource3, all conditions matched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName3], "some-oid"),
|
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName3], "some-oid"),
|
||||||
resourceMap: map[string]string{
|
resourceMap: map[string]string{
|
||||||
PropertyKeyFilePath: "any-object-name",
|
PropertyKeyFilePath: "any-object-name",
|
||||||
},
|
},
|
||||||
|
@ -711,7 +788,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket resource, user condition mismatched",
|
name: "bucket resource, user condition mismatched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"),
|
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName2], "some-oid"),
|
||||||
resourceMap: map[string]string{
|
resourceMap: map[string]string{
|
||||||
PropertyKeyFilePath: "any-object-name",
|
PropertyKeyFilePath: "any-object-name",
|
||||||
},
|
},
|
||||||
|
@ -724,7 +801,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket resource, key2 condition mismatched",
|
name: "bucket resource, key2 condition mismatched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName3], "some-oid"),
|
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName3], "some-oid"),
|
||||||
resourceMap: map[string]string{
|
resourceMap: map[string]string{
|
||||||
PropertyKeyFilePath: "any-object-name",
|
PropertyKeyFilePath: "any-object-name",
|
||||||
},
|
},
|
||||||
|
@ -738,7 +815,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket resource, key1 condition mismatched",
|
name: "bucket resource, key1 condition mismatched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"),
|
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName2], "some-oid"),
|
||||||
resourceMap: map[string]string{
|
resourceMap: map[string]string{
|
||||||
PropertyKeyFilePath: "any-object-name",
|
PropertyKeyFilePath: "any-object-name",
|
||||||
},
|
},
|
||||||
|
@ -751,7 +828,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket/object resource, all conditions matched",
|
name: "bucket/object resource, all conditions matched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"),
|
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"),
|
||||||
resourceMap: map[string]string{
|
resourceMap: map[string]string{
|
||||||
PropertyKeyFilePath: objName1,
|
PropertyKeyFilePath: objName1,
|
||||||
},
|
},
|
||||||
|
@ -765,7 +842,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket/object resource, user condition mismatched",
|
name: "bucket/object resource, user condition mismatched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"),
|
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"),
|
||||||
resourceMap: map[string]string{
|
resourceMap: map[string]string{
|
||||||
PropertyKeyFilePath: objName1,
|
PropertyKeyFilePath: objName1,
|
||||||
},
|
},
|
||||||
|
@ -779,7 +856,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket/object resource, key1 condition mismatched",
|
name: "bucket/object resource, key1 condition mismatched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"),
|
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"),
|
||||||
resourceMap: map[string]string{
|
resourceMap: map[string]string{
|
||||||
PropertyKeyFilePath: objName1,
|
PropertyKeyFilePath: objName1,
|
||||||
},
|
},
|
||||||
|
@ -792,7 +869,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket/object resource, key2 condition mismatched",
|
name: "bucket/object resource, key2 condition mismatched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"),
|
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"),
|
||||||
resourceMap: map[string]string{
|
resourceMap: map[string]string{
|
||||||
PropertyKeyFilePath: objName1,
|
PropertyKeyFilePath: objName1,
|
||||||
},
|
},
|
||||||
|
@ -806,7 +883,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket/object resource, object filepath condition mismatched",
|
name: "bucket/object resource, object filepath condition mismatched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"),
|
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"),
|
||||||
resourceMap: map[string]string{
|
resourceMap: map[string]string{
|
||||||
PropertyKeyFilePath: "any-object-name",
|
PropertyKeyFilePath: "any-object-name",
|
||||||
},
|
},
|
||||||
|
@ -849,15 +926,16 @@ func TestComplexS3Conditions(t *testing.T) {
|
||||||
principal2 := "arn:aws:iam::" + namespace + ":user/" + userName2
|
principal2 := "arn:aws:iam::" + namespace + ":user/" + userName2
|
||||||
bktName1, bktName2, bktName3 := "bktName", "bktName2", "bktName3"
|
bktName1, bktName2, bktName3 := "bktName", "bktName2", "bktName3"
|
||||||
objName1 := "objName1"
|
objName1 := "objName1"
|
||||||
resource1 := bktName1 + "/" + objName1
|
resource1 := fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName1, objName1)
|
||||||
resource2 := bktName2 + "/*"
|
resource2 := fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName2)
|
||||||
resource3 := bktName3 + "/*"
|
resource3 := fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName3)
|
||||||
action := "PutObject"
|
action := "s3:DeleteObject"
|
||||||
|
action2 := "s3:DeleteMultipleObjects"
|
||||||
|
|
||||||
key1, key2 := "key1", "key2"
|
key1, key2 := "key1", "key2"
|
||||||
val0, val1, val2 := "val0", "val1", "val2"
|
val0, val1, val2 := "val0", "val1", "val2"
|
||||||
|
|
||||||
mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3})
|
mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3}, "")
|
||||||
|
|
||||||
p := Policy{
|
p := Policy{
|
||||||
Version: "2012-10-17",
|
Version: "2012-10-17",
|
||||||
|
@ -866,8 +944,8 @@ func TestComplexS3Conditions(t *testing.T) {
|
||||||
AWSPrincipalType: {principal1, principal2},
|
AWSPrincipalType: {principal1, principal2},
|
||||||
},
|
},
|
||||||
Effect: DenyEffect,
|
Effect: DenyEffect,
|
||||||
Action: []string{"s3:" + action},
|
Action: []string{action},
|
||||||
Resource: []string{"arn:aws:s3:::" + resource1, "arn:aws:s3:::" + resource2, "arn:aws:s3:::" + resource3},
|
Resource: []string{resource1, resource2, resource3},
|
||||||
Conditions: map[string]Condition{
|
Conditions: map[string]Condition{
|
||||||
CondStringEquals: {key1: {val0, val1}},
|
CondStringEquals: {key1: {val0, val1}},
|
||||||
CondStringLike: {key2: {val2}},
|
CondStringLike: {key2: {val2}},
|
||||||
|
@ -876,7 +954,7 @@ func TestComplexS3Conditions(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedStatus := chain.AccessDenied
|
expectedStatus := chain.AccessDenied
|
||||||
expectedActions := chain.Actions{Names: actionToOpMap[action]}
|
expectedActions := chain.Actions{Names: []string{action, action2}}
|
||||||
expectedResources := chain.Resources{Names: []string{resource1, resource2, resource3}}
|
expectedResources := chain.Resources{Names: []string{resource1, resource2, resource3}}
|
||||||
|
|
||||||
user1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: s3.PropertyKeyOwner, Value: mockResolver.users[user1]}
|
user1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: s3.PropertyKeyOwner, Value: mockResolver.users[user1]}
|
||||||
|
@ -958,7 +1036,7 @@ func TestComplexS3Conditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket resource3, all conditions matched",
|
name: "bucket resource3, all conditions matched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: bktName3 + "/some-obj",
|
resource: fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName3, "some-obj"),
|
||||||
requestMap: map[string]string{
|
requestMap: map[string]string{
|
||||||
s3.PropertyKeyOwner: mockResolver.users[user1],
|
s3.PropertyKeyOwner: mockResolver.users[user1],
|
||||||
key1: val0,
|
key1: val0,
|
||||||
|
@ -969,7 +1047,7 @@ func TestComplexS3Conditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket resource, user condition mismatched",
|
name: "bucket resource, user condition mismatched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: bktName2 + "/some-obj",
|
resource: fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName2, "some-obj"),
|
||||||
requestMap: map[string]string{
|
requestMap: map[string]string{
|
||||||
key1: val0,
|
key1: val0,
|
||||||
key2: val2,
|
key2: val2,
|
||||||
|
@ -979,7 +1057,7 @@ func TestComplexS3Conditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket resource, key2 condition mismatched",
|
name: "bucket resource, key2 condition mismatched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: bktName3 + "/some-obj",
|
resource: fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName3, "some-obj"),
|
||||||
requestMap: map[string]string{
|
requestMap: map[string]string{
|
||||||
s3.PropertyKeyOwner: mockResolver.users[user1],
|
s3.PropertyKeyOwner: mockResolver.users[user1],
|
||||||
key1: val0,
|
key1: val0,
|
||||||
|
@ -990,7 +1068,7 @@ func TestComplexS3Conditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket resource, key1 condition mismatched",
|
name: "bucket resource, key1 condition mismatched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: bktName2 + "/some-obj",
|
resource: fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName2, "some-obj"),
|
||||||
requestMap: map[string]string{
|
requestMap: map[string]string{
|
||||||
s3.PropertyKeyOwner: mockResolver.users[user1],
|
s3.PropertyKeyOwner: mockResolver.users[user1],
|
||||||
key2: val2,
|
key2: val2,
|
||||||
|
@ -1011,7 +1089,7 @@ func TestComplexS3Conditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bucket/object resource, resource mismatched",
|
name: "bucket/object resource, resource mismatched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: bktName1 + "/some-obj",
|
resource: fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName1, "some-obj"),
|
||||||
requestMap: map[string]string{
|
requestMap: map[string]string{
|
||||||
s3.PropertyKeyOwner: mockResolver.users[user1],
|
s3.PropertyKeyOwner: mockResolver.users[user1],
|
||||||
key1: val0,
|
key1: val0,
|
||||||
|
@ -1054,7 +1132,7 @@ func TestComplexS3Conditions(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "resource mismatched",
|
name: "resource mismatched",
|
||||||
action: action,
|
action: action,
|
||||||
resource: "some-bkt/some-obj",
|
resource: fmt.Sprintf(s3.ResourceFormatS3BucketObject, "some-bkt", "some-obj"),
|
||||||
requestMap: map[string]string{
|
requestMap: map[string]string{
|
||||||
s3.PropertyKeyOwner: mockResolver.users[user1],
|
s3.PropertyKeyOwner: mockResolver.users[user1],
|
||||||
key1: val0,
|
key1: val0,
|
||||||
|
@ -1072,6 +1150,57 @@ func TestComplexS3Conditions(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestS3BucketResource(t *testing.T) {
|
||||||
|
namespace := "ns"
|
||||||
|
bktName1, bktName2 := "bucket1", "bucket2"
|
||||||
|
chainName := chain.Name("name")
|
||||||
|
|
||||||
|
mockResolver := newMockUserResolver([]string{}, []string{}, "")
|
||||||
|
|
||||||
|
p := Policy{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: []Statement{
|
||||||
|
{
|
||||||
|
Principal: map[PrincipalType][]string{Wildcard: nil},
|
||||||
|
Effect: DenyEffect,
|
||||||
|
Action: []string{"s3:HeadBucket"},
|
||||||
|
Resource: []string{fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName1)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Principal: map[PrincipalType][]string{Wildcard: nil},
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"*"},
|
||||||
|
Resource: []string{s3.ResourceFormatS3All},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s3Chain, err := ConvertToS3Chain(p, mockResolver)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s := inmemory.NewInMemory()
|
||||||
|
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chainName, engine.NamespaceTarget(namespace), s3Chain)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// check we match just "bucket1" resource
|
||||||
|
req := testutil.NewRequest("s3:HeadBucket", testutil.NewResource(fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName1), nil), nil)
|
||||||
|
status, _, err := s.IsAllowed(chainName, engine.NewRequestTargetWithNamespace(namespace), req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, chain.AccessDenied.String(), status.String())
|
||||||
|
|
||||||
|
// check we match just "bucket2" resource
|
||||||
|
req = testutil.NewRequest("s3:HeadBucket", testutil.NewResource(fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName2), nil), nil)
|
||||||
|
status, _, err = s.IsAllowed(chainName, engine.NewRequestTargetWithNamespace(namespace), req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, chain.Allow.String(), status.String())
|
||||||
|
|
||||||
|
// check we also match "bucket2/object" resource
|
||||||
|
req = testutil.NewRequest("s3:PutObject", testutil.NewResource(fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName2, "object"), nil), nil)
|
||||||
|
status, _, err = s.IsAllowed(chainName, engine.NewRequestTargetWithNamespace(namespace), req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, chain.Allow.String(), status.String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestWildcardConverters(t *testing.T) {
|
func TestWildcardConverters(t *testing.T) {
|
||||||
policy := `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"*","Resource":"*"}}`
|
policy := `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"*","Resource":"*"}}`
|
||||||
|
|
||||||
|
@ -1079,60 +1208,73 @@ func TestWildcardConverters(t *testing.T) {
|
||||||
err := json.Unmarshal([]byte(policy), &p)
|
err := json.Unmarshal([]byte(policy), &p)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = ConvertToS3Chain(p, newMockUserResolver(nil, nil))
|
s3Expected := &chain.Chain{
|
||||||
require.NoError(t, err)
|
Rules: []chain.Rule{{
|
||||||
|
Status: chain.Allow,
|
||||||
|
Actions: chain.Actions{Names: []string{Wildcard}},
|
||||||
|
Resources: chain.Resources{Names: []string{Wildcard}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
_, err = ConvertToNativeChain(p, newMockUserResolver(nil, nil))
|
s3Chain, err := ConvertToS3Chain(p, newMockUserResolver(nil, nil, ""))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, s3Expected, s3Chain)
|
||||||
|
|
||||||
|
nativeExpected := &chain.Chain{
|
||||||
|
Rules: []chain.Rule{{
|
||||||
|
Status: chain.Allow,
|
||||||
|
Actions: chain.Actions{Names: []string{Wildcard}},
|
||||||
|
Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects, native.ResourceFormatAllContainers}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeChain, err := ConvertToNativeChain(p, newMockUserResolver(nil, nil, ""))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, nativeExpected, nativeChain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestActionParsing(t *testing.T) {
|
func TestActionParsing(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
action string
|
action string
|
||||||
expected string
|
err bool
|
||||||
err bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
action: "withoutPrefix",
|
action: "withoutPrefix",
|
||||||
expected: "",
|
err: true,
|
||||||
err: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "s3:*Object",
|
action: "s3:*Object",
|
||||||
expected: "",
|
err: true,
|
||||||
err: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "*",
|
action: "*",
|
||||||
expected: "*",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "s3:PutObject",
|
action: "s3:PutObject",
|
||||||
expected: "PutObject",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "s3:Put*",
|
action: "s3:Put*",
|
||||||
expected: "Put*",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "s3:*",
|
action: "s3:*",
|
||||||
expected: "*",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "s3:",
|
action: "s3:",
|
||||||
|
},
|
||||||
expected: "",
|
{
|
||||||
|
action: "iam:ListAccessKeys",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "iam:*",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
actual, err := parseActionAsS3Action(tc.action)
|
err := validateAction(tc.action)
|
||||||
if tc.err {
|
if tc.err {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
return
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.expected, actual)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1202,55 +1344,24 @@ func TestPrincipalParsing(t *testing.T) {
|
||||||
|
|
||||||
func TestResourceParsing(t *testing.T) {
|
func TestResourceParsing(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
resource string
|
resource string
|
||||||
expectedBucket string
|
err bool
|
||||||
expectedObject string
|
|
||||||
err bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{resource: "withoutPrefixAnd", err: true},
|
||||||
resource: "withoutPrefixAnd",
|
{resource: "arn:aws:s3:::*/obj", err: true},
|
||||||
err: true,
|
{resource: "arn:aws:s3:::bkt/*"},
|
||||||
},
|
{resource: "arn:aws:s3:::bkt"},
|
||||||
{
|
{resource: "arn:aws:s3:::bkt/"},
|
||||||
resource: "arn:aws:s3:::*/obj",
|
{resource: "arn:aws:s3:::*"},
|
||||||
err: true,
|
{resource: "*"},
|
||||||
},
|
|
||||||
{
|
|
||||||
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) {
|
t.Run("", func(t *testing.T) {
|
||||||
bkt, obj, err := parseResourceAsS3ARN(tc.resource)
|
err := validateResource(tc.resource)
|
||||||
if tc.err {
|
if tc.err {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
return
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.expectedBucket, bkt)
|
|
||||||
require.Equal(t, tc.expectedObject, obj)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,14 @@ package iam
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||||
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
||||||
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory"
|
||||||
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil"
|
||||||
|
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -428,3 +434,70 @@ func TestValidatePolicies(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProcessDenyFirst(t *testing.T) {
|
||||||
|
identityBasedPolicyStr := `
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Principal": {
|
||||||
|
"AWS": [ "arn:aws:iam::root:user/user-name" ]
|
||||||
|
},
|
||||||
|
"Action": ["s3:PutObject" ],
|
||||||
|
"Resource": "arn:aws:s3:::*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
resourceBasedPolicyStr := `
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Deny",
|
||||||
|
"Principal": "*",
|
||||||
|
"Action": "s3:*",
|
||||||
|
"Resource": [ "arn:aws:s3:::test-bucket/*" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var identityPolicy Policy
|
||||||
|
err := json.Unmarshal([]byte(identityBasedPolicyStr), &identityPolicy)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var resourcePolicy Policy
|
||||||
|
err = json.Unmarshal([]byte(resourceBasedPolicyStr), &resourcePolicy)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mockResolver := newMockUserResolver([]string{"root/user-name"}, []string{"test-bucket"}, "")
|
||||||
|
|
||||||
|
identityNativePolicy, err := ConvertToNativeChain(identityPolicy, mockResolver)
|
||||||
|
require.NoError(t, err)
|
||||||
|
identityNativePolicy.MatchType = chain.MatchTypeFirstMatch
|
||||||
|
|
||||||
|
resourceNativePolicy, err := ConvertToNativeChain(resourcePolicy, mockResolver)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s := inmemory.NewInMemory()
|
||||||
|
|
||||||
|
target := engine.NamespaceTarget("ns")
|
||||||
|
|
||||||
|
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, identityNativePolicy)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, resourceNativePolicy)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resource := testutil.NewResource(fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers["test-bucket"]), nil)
|
||||||
|
request := testutil.NewRequest("PutObject", resource, map[string]string{native.PropertyKeyActorPublicKey: mockResolver.users["root/user-name"]})
|
||||||
|
|
||||||
|
status, found, err := s.IsAllowed(chain.Ingress, engine.NewRequestTarget("ns", ""), request)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, found)
|
||||||
|
require.Equal(t, chain.AccessDenied, status)
|
||||||
|
}
|
||||||
|
|
|
@ -1,25 +1,38 @@
|
||||||
package chain
|
package chain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/util"
|
"git.frostfs.info/TrueCloudLab/policy-engine/util"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ID is the ID of rule chain.
|
// ID is the ID of rule chain.
|
||||||
type ID string
|
type ID []byte
|
||||||
|
|
||||||
|
// MatchType is the match type for chain rules.
|
||||||
|
type MatchType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MatchTypeDenyPriority rejects the request if any `Deny` is specified.
|
||||||
|
MatchTypeDenyPriority MatchType = 0
|
||||||
|
// MatchTypeFirstMatch returns the first rule action matched to the request.
|
||||||
|
MatchTypeFirstMatch MatchType = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
//easyjson:json
|
||||||
type Chain struct {
|
type Chain struct {
|
||||||
ID ID
|
ID ID
|
||||||
|
|
||||||
Rules []Rule
|
Rules []Rule
|
||||||
|
|
||||||
|
MatchType MatchType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chain) Bytes() []byte {
|
func (c *Chain) Bytes() []byte {
|
||||||
data, err := json.Marshal(c)
|
data, err := c.MarshalBinary()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +40,7 @@ func (c *Chain) Bytes() []byte {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chain) DecodeBytes(b []byte) error {
|
func (c *Chain) DecodeBytes(b []byte) error {
|
||||||
return json.Unmarshal(b, c)
|
return c.UnmarshalBinary(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Rule struct {
|
type Rule struct {
|
||||||
|
@ -64,6 +77,8 @@ type ObjectType byte
|
||||||
const (
|
const (
|
||||||
ObjectResource ObjectType = iota
|
ObjectResource ObjectType = iota
|
||||||
ObjectRequest
|
ObjectRequest
|
||||||
|
ContainerResource
|
||||||
|
ContainerRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConditionType byte
|
type ConditionType byte
|
||||||
|
@ -91,45 +106,48 @@ const (
|
||||||
CondNumericLessThanEquals
|
CondNumericLessThanEquals
|
||||||
CondNumericGreaterThan
|
CondNumericGreaterThan
|
||||||
CondNumericGreaterThanEquals
|
CondNumericGreaterThanEquals
|
||||||
|
|
||||||
|
CondSliceContains
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var condToStr = []struct {
|
||||||
|
ct ConditionType
|
||||||
|
str string
|
||||||
|
}{
|
||||||
|
{CondStringEquals, "StringEquals"},
|
||||||
|
{CondStringNotEquals, "StringNotEquals"},
|
||||||
|
{CondStringEqualsIgnoreCase, "StringEqualsIgnoreCase"},
|
||||||
|
{CondStringNotEqualsIgnoreCase, "StringNotEqualsIgnoreCase"},
|
||||||
|
{CondStringLike, "StringLike"},
|
||||||
|
{CondStringNotLike, "StringNotLike"},
|
||||||
|
{CondStringLessThan, "StringLessThan"},
|
||||||
|
{CondStringLessThanEquals, "StringLessThanEquals"},
|
||||||
|
{CondStringGreaterThan, "StringGreaterThan"},
|
||||||
|
{CondStringGreaterThanEquals, "StringGreaterThanEquals"},
|
||||||
|
{CondNumericEquals, "NumericEquals"},
|
||||||
|
{CondNumericNotEquals, "NumericNotEquals"},
|
||||||
|
{CondNumericLessThan, "NumericLessThan"},
|
||||||
|
{CondNumericLessThanEquals, "NumericLessThanEquals"},
|
||||||
|
{CondNumericGreaterThan, "NumericGreaterThan"},
|
||||||
|
{CondNumericGreaterThanEquals, "NumericGreaterThanEquals"},
|
||||||
|
{CondSliceContains, "SliceContains"},
|
||||||
|
}
|
||||||
|
|
||||||
func (c ConditionType) String() string {
|
func (c ConditionType) String() string {
|
||||||
switch c {
|
for _, v := range condToStr {
|
||||||
case CondStringEquals:
|
if v.ct == c {
|
||||||
return "StringEquals"
|
return v.str
|
||||||
case CondStringNotEquals:
|
}
|
||||||
return "StringNotEquals"
|
|
||||||
case CondStringEqualsIgnoreCase:
|
|
||||||
return "StringEqualsIgnoreCase"
|
|
||||||
case CondStringNotEqualsIgnoreCase:
|
|
||||||
return "StringNotEqualsIgnoreCase"
|
|
||||||
case CondStringLike:
|
|
||||||
return "StringLike"
|
|
||||||
case CondStringNotLike:
|
|
||||||
return "StringNotLike"
|
|
||||||
case CondStringLessThan:
|
|
||||||
return "StringLessThan"
|
|
||||||
case CondStringLessThanEquals:
|
|
||||||
return "StringLessThanEquals"
|
|
||||||
case CondStringGreaterThan:
|
|
||||||
return "StringGreaterThan"
|
|
||||||
case CondStringGreaterThanEquals:
|
|
||||||
return "StringGreaterThanEquals"
|
|
||||||
case CondNumericEquals:
|
|
||||||
return "NumericEquals"
|
|
||||||
case CondNumericNotEquals:
|
|
||||||
return "NumericNotEquals"
|
|
||||||
case CondNumericLessThan:
|
|
||||||
return "NumericLessThan"
|
|
||||||
case CondNumericLessThanEquals:
|
|
||||||
return "NumericLessThanEquals"
|
|
||||||
case CondNumericGreaterThan:
|
|
||||||
return "NumericGreaterThan"
|
|
||||||
case CondNumericGreaterThanEquals:
|
|
||||||
return "NumericGreaterThanEquals"
|
|
||||||
default:
|
|
||||||
return "unknown condition type"
|
|
||||||
}
|
}
|
||||||
|
return "unknown condition type"
|
||||||
|
}
|
||||||
|
|
||||||
|
const condSliceContainsDelimiter = "\x00"
|
||||||
|
|
||||||
|
// FormCondSliceContainsValue builds value for ObjectResource or ObjectRequest property
|
||||||
|
// that can be matched by CondSliceContains condition.
|
||||||
|
func FormCondSliceContainsValue(values []string) string {
|
||||||
|
return strings.Join(values, condSliceContainsDelimiter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Condition) Match(req resource.Request) bool {
|
func (c *Condition) Match(req resource.Request) bool {
|
||||||
|
@ -166,6 +184,8 @@ func (c *Condition) Match(req resource.Request) bool {
|
||||||
return val > c.Value
|
return val > c.Value
|
||||||
case CondStringGreaterThanEquals:
|
case CondStringGreaterThanEquals:
|
||||||
return val >= c.Value
|
return val >= c.Value
|
||||||
|
case CondSliceContains:
|
||||||
|
return slices.Contains(strings.Split(val, condSliceContainsDelimiter), c.Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,6 +234,17 @@ func (r *Rule) matchAll(obj resource.Request) (status Status, matched bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chain) Match(req resource.Request) (status Status, matched bool) {
|
func (c *Chain) Match(req resource.Request) (status Status, matched bool) {
|
||||||
|
switch c.MatchType {
|
||||||
|
case MatchTypeDenyPriority:
|
||||||
|
return c.denyPriority(req)
|
||||||
|
case MatchTypeFirstMatch:
|
||||||
|
return c.firstMatch(req)
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unknown MatchType %d", c.MatchType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chain) firstMatch(req resource.Request) (status Status, matched bool) {
|
||||||
for i := range c.Rules {
|
for i := range c.Rules {
|
||||||
status, matched := c.Rules[i].Match(req)
|
status, matched := c.Rules[i].Match(req)
|
||||||
if matched {
|
if matched {
|
||||||
|
@ -222,3 +253,21 @@ func (c *Chain) Match(req resource.Request) (status Status, matched bool) {
|
||||||
}
|
}
|
||||||
return NoRuleFound, false
|
return NoRuleFound, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Chain) denyPriority(req resource.Request) (status Status, matched bool) {
|
||||||
|
var allowFound bool
|
||||||
|
for i := range c.Rules {
|
||||||
|
status, matched := c.Rules[i].Match(req)
|
||||||
|
if !matched {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if status != Allow {
|
||||||
|
return status, true
|
||||||
|
}
|
||||||
|
allowFound = true
|
||||||
|
}
|
||||||
|
if allowFound {
|
||||||
|
return Allow, true
|
||||||
|
}
|
||||||
|
return NoRuleFound, false
|
||||||
|
}
|
||||||
|
|
BIN
pkg/chain/chain_easyjson.go
generated
Normal file
BIN
pkg/chain/chain_easyjson.go
generated
Normal file
Binary file not shown.
|
@ -7,4 +7,7 @@ const (
|
||||||
// Ingress represents chains applied when crossing user/storage network boundary.
|
// Ingress represents chains applied when crossing user/storage network boundary.
|
||||||
// It is not applied when talking between nodes.
|
// It is not applied when talking between nodes.
|
||||||
Ingress Name = "ingress"
|
Ingress Name = "ingress"
|
||||||
|
|
||||||
|
// S3 represents chains applied when crossing user/s3 network boundary.
|
||||||
|
S3 Name = "s3"
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,11 +3,28 @@ package chain
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil"
|
||||||
|
"git.frostfs.info/TrueCloudLab/policy-engine/schema/common"
|
||||||
|
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestChainIDSerialization(t *testing.T) {
|
||||||
|
chainIDBytes := []byte{93, 236, 80, 138, 168, 3, 144, 92, 173, 141, 16, 42, 249, 90, 97, 109, 211, 169, 54, 163}
|
||||||
|
|
||||||
|
chain1 := &Chain{ID: ID(chainIDBytes)}
|
||||||
|
data := chain1.Bytes()
|
||||||
|
|
||||||
|
var chain2 Chain
|
||||||
|
err := chain2.DecodeBytes(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, chain1.ID, chain2.ID)
|
||||||
|
}
|
||||||
|
|
||||||
func TestEncodeDecode(t *testing.T) {
|
func TestEncodeDecode(t *testing.T) {
|
||||||
expected := Chain{
|
expected := Chain{
|
||||||
|
MatchType: MatchTypeFirstMatch,
|
||||||
Rules: []Rule{
|
Rules: []Rule{
|
||||||
{
|
{
|
||||||
Status: Allow,
|
Status: Allow,
|
||||||
|
@ -31,3 +48,104 @@ func TestEncodeDecode(t *testing.T) {
|
||||||
require.NoError(t, actual.DecodeBytes(data))
|
require.NoError(t, actual.DecodeBytes(data))
|
||||||
require.Equal(t, expected, actual)
|
require.Equal(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReturnFirstMatch(t *testing.T) {
|
||||||
|
ch := Chain{
|
||||||
|
Rules: []Rule{
|
||||||
|
{
|
||||||
|
Status: Allow,
|
||||||
|
Actions: Actions{Names: []string{
|
||||||
|
native.MethodPutObject,
|
||||||
|
}},
|
||||||
|
Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
|
||||||
|
Condition: []Condition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Status: AccessDenied,
|
||||||
|
Actions: Actions{Names: []string{
|
||||||
|
native.MethodPutObject,
|
||||||
|
}},
|
||||||
|
Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
|
||||||
|
Condition: []Condition{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
|
||||||
|
request := testutil.NewRequest(native.MethodPutObject, resource, nil)
|
||||||
|
|
||||||
|
t.Run("default match", func(t *testing.T) {
|
||||||
|
st, found := ch.Match(request)
|
||||||
|
require.True(t, found)
|
||||||
|
require.Equal(t, AccessDenied, st)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("return first match", func(t *testing.T) {
|
||||||
|
ch.MatchType = MatchTypeFirstMatch
|
||||||
|
st, found := ch.Match(request)
|
||||||
|
require.True(t, found)
|
||||||
|
require.Equal(t, Allow, st)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCondSliceContainsMatch(t *testing.T) {
|
||||||
|
propKey := common.PropertyKeyFrostFSIDGroupID
|
||||||
|
groupID := "1"
|
||||||
|
|
||||||
|
ch := Chain{Rules: []Rule{{
|
||||||
|
Status: Allow,
|
||||||
|
Actions: Actions{Names: []string{native.MethodPutObject}},
|
||||||
|
Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
|
||||||
|
Condition: []Condition{{
|
||||||
|
Op: CondSliceContains,
|
||||||
|
Object: ObjectRequest,
|
||||||
|
Key: propKey,
|
||||||
|
Value: groupID,
|
||||||
|
}},
|
||||||
|
}}}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
status Status
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple value",
|
||||||
|
value: groupID,
|
||||||
|
status: Allow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple value by func",
|
||||||
|
value: FormCondSliceContainsValue([]string{groupID}),
|
||||||
|
status: Allow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple values by func",
|
||||||
|
value: FormCondSliceContainsValue([]string{groupID, "2", "3"}),
|
||||||
|
status: Allow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple mismatched",
|
||||||
|
value: "3",
|
||||||
|
status: NoRuleFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple mismatched",
|
||||||
|
value: FormCondSliceContainsValue([]string{"11", "12"}),
|
||||||
|
status: NoRuleFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comma correct handling mismatched",
|
||||||
|
value: "1,11",
|
||||||
|
status: NoRuleFound,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
|
||||||
|
request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{propKey: tc.value})
|
||||||
|
|
||||||
|
st, _ := ch.Match(request)
|
||||||
|
require.Equal(t, tc.status.String(), st.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
257
pkg/chain/marshal_binary.go
Normal file
257
pkg/chain/marshal_binary.go
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
package chain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/marshal"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChainMarshalVersion uint8 = 0 // increase if breaking change
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ encoding.BinaryMarshaler = (*Chain)(nil)
|
||||||
|
_ encoding.BinaryUnmarshaler = (*Chain)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Chain) MarshalBinary() ([]byte, error) {
|
||||||
|
s := marshal.UInt8Size // Marshaller version
|
||||||
|
s += marshal.UInt8Size // Chain version
|
||||||
|
s += marshal.SliceSize(c.ID, func(byte) int { return marshal.ByteSize })
|
||||||
|
s += marshal.SliceSize(c.Rules, ruleSize)
|
||||||
|
s += marshal.UInt8Size // MatchType
|
||||||
|
|
||||||
|
buf := make([]byte, s)
|
||||||
|
var offset int
|
||||||
|
var err error
|
||||||
|
offset, err = marshal.UInt8Marshal(buf, offset, marshal.Version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
offset, err = marshal.UInt8Marshal(buf, offset, ChainMarshalVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
offset, err = marshal.SliceMarshal(buf, offset, c.ID, marshal.ByteMarshal)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
offset, err = marshal.SliceMarshal(buf, offset, c.Rules, marshalRule)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
offset, err = marshal.UInt8Marshal(buf, offset, uint8(c.MatchType))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := marshal.VerifyMarshal(buf, offset); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chain) UnmarshalBinary(data []byte) error {
|
||||||
|
var offset int
|
||||||
|
|
||||||
|
marshallerVersion, offset, err := marshal.UInt8Unmarshal(data, offset)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if marshallerVersion != marshal.Version {
|
||||||
|
return fmt.Errorf("unsupported marshaller version %d", marshallerVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
chainVersion, offset, err := marshal.UInt8Unmarshal(data, offset)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if chainVersion != ChainMarshalVersion {
|
||||||
|
return fmt.Errorf("unsupported chain version %d", chainVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
idBytes, offset, err := marshal.SliceUnmarshal(data, offset, marshal.ByteUnmarshal)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.ID = ID(idBytes)
|
||||||
|
|
||||||
|
c.Rules, offset, err = marshal.SliceUnmarshal(data, offset, unmarshalRule)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
matchTypeV, offset, err := marshal.UInt8Unmarshal(data, offset)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.MatchType = MatchType(matchTypeV)
|
||||||
|
|
||||||
|
return marshal.VerifyUnmarshal(data, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ruleSize(r Rule) int {
|
||||||
|
s := marshal.ByteSize // Status
|
||||||
|
s += actionsSize(r.Actions)
|
||||||
|
s += resourcesSize(r.Resources)
|
||||||
|
s += marshal.BoolSize // Any
|
||||||
|
s += marshal.SliceSize(r.Condition, conditionSize)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalRule(buf []byte, offset int, r Rule) (int, error) {
|
||||||
|
offset, err := marshal.ByteMarshal(buf, offset, byte(r.Status))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
offset, err = marshalActions(buf, offset, r.Actions)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
offset, err = marshalResources(buf, offset, r.Resources)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
offset, err = marshal.BoolMarshal(buf, offset, r.Any)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return marshal.SliceMarshal(buf, offset, r.Condition, marshalCondition)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalRule(buf []byte, offset int) (Rule, int, error) {
|
||||||
|
var r Rule
|
||||||
|
statusV, offset, err := marshal.ByteUnmarshal(buf, offset)
|
||||||
|
if err != nil {
|
||||||
|
return Rule{}, 0, err
|
||||||
|
}
|
||||||
|
r.Status = Status(statusV)
|
||||||
|
|
||||||
|
r.Actions, offset, err = unmarshalActions(buf, offset)
|
||||||
|
if err != nil {
|
||||||
|
return Rule{}, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Resources, offset, err = unmarshalResources(buf, offset)
|
||||||
|
if err != nil {
|
||||||
|
return Rule{}, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Any, offset, err = marshal.BoolUnmarshal(buf, offset)
|
||||||
|
if err != nil {
|
||||||
|
return Rule{}, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Condition, offset, err = marshal.SliceUnmarshal(buf, offset, unmarshalCondition)
|
||||||
|
if err != nil {
|
||||||
|
return Rule{}, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func actionsSize(a Actions) int {
|
||||||
|
return marshal.BoolSize + // Inverted
|
||||||
|
marshal.SliceSize(a.Names, marshal.StringSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalActions(buf []byte, offset int, a Actions) (int, error) {
|
||||||
|
offset, err := marshal.BoolMarshal(buf, offset, a.Inverted)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return marshal.SliceMarshal(buf, offset, a.Names, marshal.StringMarshal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalActions(buf []byte, offset int) (Actions, int, error) {
|
||||||
|
var a Actions
|
||||||
|
var err error
|
||||||
|
a.Inverted, offset, err = marshal.BoolUnmarshal(buf, offset)
|
||||||
|
if err != nil {
|
||||||
|
return Actions{}, 0, err
|
||||||
|
}
|
||||||
|
a.Names, offset, err = marshal.SliceUnmarshal(buf, offset, marshal.StringUnmarshal)
|
||||||
|
if err != nil {
|
||||||
|
return Actions{}, 0, err
|
||||||
|
}
|
||||||
|
return a, offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourcesSize(r Resources) int {
|
||||||
|
return marshal.BoolSize + // Inverted
|
||||||
|
marshal.SliceSize(r.Names, marshal.StringSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalResources(buf []byte, offset int, r Resources) (int, error) {
|
||||||
|
offset, err := marshal.BoolMarshal(buf, offset, r.Inverted)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return marshal.SliceMarshal(buf, offset, r.Names, marshal.StringMarshal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalResources(buf []byte, offset int) (Resources, int, error) {
|
||||||
|
var r Resources
|
||||||
|
var err error
|
||||||
|
r.Inverted, offset, err = marshal.BoolUnmarshal(buf, offset)
|
||||||
|
if err != nil {
|
||||||
|
return Resources{}, 0, err
|
||||||
|
}
|
||||||
|
r.Names, offset, err = marshal.SliceUnmarshal(buf, offset, marshal.StringUnmarshal)
|
||||||
|
if err != nil {
|
||||||
|
return Resources{}, 0, err
|
||||||
|
}
|
||||||
|
return r, offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func conditionSize(c Condition) int {
|
||||||
|
return marshal.ByteSize + // Op
|
||||||
|
marshal.ByteSize + // Object
|
||||||
|
marshal.StringSize(c.Key) +
|
||||||
|
marshal.StringSize(c.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalCondition(buf []byte, offset int, c Condition) (int, error) {
|
||||||
|
offset, err := marshal.ByteMarshal(buf, offset, byte(c.Op))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
offset, err = marshal.ByteMarshal(buf, offset, byte(c.Object))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
offset, err = marshal.StringMarshal(buf, offset, c.Key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return marshal.StringMarshal(buf, offset, c.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalCondition(buf []byte, offset int) (Condition, int, error) {
|
||||||
|
var c Condition
|
||||||
|
opV, offset, err := marshal.ByteUnmarshal(buf, offset)
|
||||||
|
if err != nil {
|
||||||
|
return Condition{}, 0, err
|
||||||
|
}
|
||||||
|
c.Op = ConditionType(opV)
|
||||||
|
|
||||||
|
obV, offset, err := marshal.ByteUnmarshal(buf, offset)
|
||||||
|
if err != nil {
|
||||||
|
return Condition{}, 0, err
|
||||||
|
}
|
||||||
|
c.Object = ObjectType(obV)
|
||||||
|
|
||||||
|
c.Key, offset, err = marshal.StringUnmarshal(buf, offset)
|
||||||
|
if err != nil {
|
||||||
|
return Condition{}, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Value, offset, err = marshal.StringUnmarshal(buf, offset)
|
||||||
|
if err != nil {
|
||||||
|
return Condition{}, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, offset, nil
|
||||||
|
}
|
272
pkg/chain/marshal_binary_test.go
Normal file
272
pkg/chain/marshal_binary_test.go
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
package chain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestChainMarshalling(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
for _, id := range generateTestIDs() {
|
||||||
|
for _, rules := range generateTestRules() {
|
||||||
|
for _, matchType := range generateTestMatchTypes() {
|
||||||
|
performMarshalTest(t, id, rules, matchType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidChainData(t *testing.T) {
|
||||||
|
var ch Chain
|
||||||
|
require.Error(t, ch.UnmarshalBinary(nil))
|
||||||
|
require.Error(t, ch.UnmarshalBinary([]byte{}))
|
||||||
|
require.Error(t, ch.UnmarshalBinary([]byte{1, 2, 3}))
|
||||||
|
require.Error(t, ch.UnmarshalBinary([]byte("\x00\x00:aws:iam::namespace:group/so\x82\x82\x82\x82\x82\x82u\x82")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzUnmarshal(f *testing.F) {
|
||||||
|
for _, id := range generateTestIDs() {
|
||||||
|
for _, rules := range generateTestRules() {
|
||||||
|
for _, matchType := range generateTestMatchTypes() {
|
||||||
|
|
||||||
|
chain := Chain{
|
||||||
|
ID: id,
|
||||||
|
Rules: rules,
|
||||||
|
MatchType: matchType,
|
||||||
|
}
|
||||||
|
data, err := chain.MarshalBinary()
|
||||||
|
require.NoError(f, err)
|
||||||
|
f.Add(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
|
var ch Chain
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
_ = ch.UnmarshalBinary(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func performMarshalTest(t *testing.T, id ID, r []Rule, mt MatchType) {
|
||||||
|
chain := Chain{
|
||||||
|
ID: id,
|
||||||
|
Rules: r,
|
||||||
|
MatchType: mt,
|
||||||
|
}
|
||||||
|
data, err := chain.MarshalBinary()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var unmarshalledChain Chain
|
||||||
|
require.NoError(t, unmarshalledChain.UnmarshalBinary(data))
|
||||||
|
|
||||||
|
require.Equal(t, chain, unmarshalledChain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTestIDs() []ID {
|
||||||
|
return []ID{
|
||||||
|
ID(""),
|
||||||
|
ID(uuid.New().String()),
|
||||||
|
ID("*::/"),
|
||||||
|
ID("avada kedavra"),
|
||||||
|
ID("arn:aws:iam::namespace:group/some_group"),
|
||||||
|
ID("$Object:homomorphicHash"),
|
||||||
|
ID("native:container/ns/9LPLUFZpEmfidG4n44vi2cjXKXSqWT492tCvLJiJ8W1J"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTestRules() [][]Rule {
|
||||||
|
result := [][]Rule{
|
||||||
|
nil,
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, st := range generateTestStatuses() {
|
||||||
|
for _, act := range generateTestActions() {
|
||||||
|
for _, res := range generateTestResources() {
|
||||||
|
for _, cond := range generateTestConditions() {
|
||||||
|
result[2] = append(result[2], Rule{
|
||||||
|
Status: st,
|
||||||
|
Actions: act,
|
||||||
|
Resources: res,
|
||||||
|
Condition: cond,
|
||||||
|
Any: true,
|
||||||
|
})
|
||||||
|
result[2] = append(result[2], Rule{
|
||||||
|
Status: st,
|
||||||
|
Actions: act,
|
||||||
|
Resources: res,
|
||||||
|
Condition: cond,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTestStatuses() []Status {
|
||||||
|
return []Status{
|
||||||
|
Allow,
|
||||||
|
NoRuleFound,
|
||||||
|
AccessDenied,
|
||||||
|
QuotaLimitReached,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTestActions() []Actions {
|
||||||
|
return []Actions{
|
||||||
|
{
|
||||||
|
Inverted: true,
|
||||||
|
Names: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Names: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Inverted: true,
|
||||||
|
Names: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Names: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Inverted: true,
|
||||||
|
Names: []string{native.MethodPutObject},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Names: []string{native.MethodPutObject},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Inverted: true,
|
||||||
|
Names: []string{native.MethodPutObject, native.MethodDeleteContainer, native.MethodDeleteObject},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Names: []string{native.MethodPutObject, native.MethodDeleteContainer, native.MethodDeleteObject},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTestResources() []Resources {
|
||||||
|
return []Resources{
|
||||||
|
{
|
||||||
|
Inverted: true,
|
||||||
|
Names: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Names: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Inverted: true,
|
||||||
|
Names: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Names: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Inverted: true,
|
||||||
|
Names: []string{native.ResourceFormatAllObjects},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Names: []string{native.ResourceFormatAllObjects},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Inverted: true,
|
||||||
|
Names: []string{
|
||||||
|
native.ResourceFormatAllObjects,
|
||||||
|
fmt.Sprintf(native.ResourceFormatRootContainer, "9LPLUFZpEmfidG4n44vi2cjXKXSqWT492tCvLJiJ8W1J"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Names: []string{
|
||||||
|
native.ResourceFormatAllObjects,
|
||||||
|
fmt.Sprintf(native.ResourceFormatRootContainer, "9LPLUFZpEmfidG4n44vi2cjXKXSqWT492tCvLJiJ8W1J"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTestConditions() [][]Condition {
|
||||||
|
result := [][]Condition{
|
||||||
|
nil,
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ct := range generateTestConditionTypes() {
|
||||||
|
for _, ot := range generateObjectTypes() {
|
||||||
|
result[2] = append(result[2], Condition{
|
||||||
|
Op: ct,
|
||||||
|
Object: ot,
|
||||||
|
Key: "",
|
||||||
|
Value: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
result[2] = append(result[2], Condition{
|
||||||
|
Op: ct,
|
||||||
|
Object: ot,
|
||||||
|
Key: "key",
|
||||||
|
Value: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
result[2] = append(result[2], Condition{
|
||||||
|
Op: ct,
|
||||||
|
Object: ot,
|
||||||
|
Key: "",
|
||||||
|
Value: "value",
|
||||||
|
})
|
||||||
|
|
||||||
|
result[2] = append(result[2], Condition{
|
||||||
|
Op: ct,
|
||||||
|
Object: ot,
|
||||||
|
Key: "key",
|
||||||
|
Value: "value",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTestConditionTypes() []ConditionType {
|
||||||
|
return []ConditionType{
|
||||||
|
CondStringEquals,
|
||||||
|
CondStringNotEquals,
|
||||||
|
CondStringEqualsIgnoreCase,
|
||||||
|
CondStringNotEqualsIgnoreCase,
|
||||||
|
CondStringLike,
|
||||||
|
CondStringNotLike,
|
||||||
|
CondStringLessThan,
|
||||||
|
CondStringLessThanEquals,
|
||||||
|
CondStringGreaterThan,
|
||||||
|
CondStringGreaterThanEquals,
|
||||||
|
CondNumericEquals,
|
||||||
|
CondNumericNotEquals,
|
||||||
|
CondNumericLessThan,
|
||||||
|
CondNumericLessThanEquals,
|
||||||
|
CondNumericGreaterThan,
|
||||||
|
CondNumericGreaterThanEquals,
|
||||||
|
CondSliceContains,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateObjectTypes() []ObjectType {
|
||||||
|
return []ObjectType{
|
||||||
|
ObjectResource,
|
||||||
|
ObjectRequest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTestMatchTypes() []MatchType {
|
||||||
|
return []MatchType{
|
||||||
|
MatchTypeDenyPriority,
|
||||||
|
MatchTypeFirstMatch,
|
||||||
|
}
|
||||||
|
}
|
145
pkg/chain/marshal_json.go
Normal file
145
pkg/chain/marshal_json.go
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
package chain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
jlexer "github.com/mailru/easyjson/jlexer"
|
||||||
|
jwriter "github.com/mailru/easyjson/jwriter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run `make generate`` if types added or changed
|
||||||
|
|
||||||
|
var matchTypeToJSONValue = []struct {
|
||||||
|
mt MatchType
|
||||||
|
str string
|
||||||
|
}{
|
||||||
|
{MatchTypeDenyPriority, "DenyPriority"},
|
||||||
|
{MatchTypeFirstMatch, "FirstMatch"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusToJSONValue = []struct {
|
||||||
|
s Status
|
||||||
|
str string
|
||||||
|
}{
|
||||||
|
{Allow, "Allow"},
|
||||||
|
{NoRuleFound, "NoRuleFound"},
|
||||||
|
{AccessDenied, "AccessDenied"},
|
||||||
|
{QuotaLimitReached, "QuotaLimitReached"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var objectTypeToJSONValue = []struct {
|
||||||
|
t ObjectType
|
||||||
|
str string
|
||||||
|
}{
|
||||||
|
{ObjectRequest, "Request"},
|
||||||
|
{ObjectResource, "Resource"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt MatchType) MarshalEasyJSON(w *jwriter.Writer) {
|
||||||
|
for _, p := range matchTypeToJSONValue {
|
||||||
|
if p.mt == mt {
|
||||||
|
w.String(p.str)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.String(strconv.FormatUint(uint64(mt), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *MatchType) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
|
str := l.String()
|
||||||
|
for _, p := range matchTypeToJSONValue {
|
||||||
|
if p.str == str {
|
||||||
|
*mt = p.mt
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := strconv.ParseUint(str, 10, 8)
|
||||||
|
if err != nil {
|
||||||
|
l.AddError(fmt.Errorf("failed to parse match type: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*mt = MatchType(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (st Status) MarshalEasyJSON(w *jwriter.Writer) {
|
||||||
|
for _, p := range statusToJSONValue {
|
||||||
|
if p.s == st {
|
||||||
|
w.String(p.str)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.String(strconv.FormatUint(uint64(st), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (st *Status) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
|
str := l.String()
|
||||||
|
for _, p := range statusToJSONValue {
|
||||||
|
if p.str == str {
|
||||||
|
*st = p.s
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := strconv.ParseUint(str, 10, 8)
|
||||||
|
if err != nil {
|
||||||
|
l.AddError(fmt.Errorf("failed to parse status: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*st = Status(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ot ObjectType) MarshalEasyJSON(w *jwriter.Writer) {
|
||||||
|
for _, p := range objectTypeToJSONValue {
|
||||||
|
if p.t == ot {
|
||||||
|
w.String(p.str)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.String(strconv.FormatUint(uint64(ot), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ot *ObjectType) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
|
str := l.String()
|
||||||
|
for _, p := range objectTypeToJSONValue {
|
||||||
|
if p.str == str {
|
||||||
|
*ot = p.t
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := strconv.ParseUint(str, 10, 8)
|
||||||
|
if err != nil {
|
||||||
|
l.AddError(fmt.Errorf("failed to parse object type: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*ot = ObjectType(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct ConditionType) MarshalEasyJSON(w *jwriter.Writer) {
|
||||||
|
for _, p := range condToStr {
|
||||||
|
if p.ct == ct {
|
||||||
|
w.String(p.str)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.String(strconv.FormatUint(uint64(ct), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *ConditionType) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
|
str := l.String()
|
||||||
|
for _, p := range condToStr {
|
||||||
|
if p.str == str {
|
||||||
|
*ct = p.ct
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := strconv.ParseUint(str, 10, 8)
|
||||||
|
if err != nil {
|
||||||
|
l.AddError(fmt.Errorf("failed to parse condition type: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*ct = ConditionType(v)
|
||||||
|
}
|
121
pkg/chain/marshal_json_test.go
Normal file
121
pkg/chain/marshal_json_test.go
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
package chain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestID(t *testing.T) {
|
||||||
|
key, err := keys.NewPrivateKeyFromWIF("L5eVx6HcHaFpQpvjQ3fy29uKDZ8rQ34bfMVx4XfZMm52EqafpNMg") // s3-gw key
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
chain1 := &Chain{ID: ID(key.PublicKey().GetScriptHash().BytesBE())}
|
||||||
|
data := chain1.Bytes()
|
||||||
|
|
||||||
|
var chain2 Chain
|
||||||
|
require.NoError(t, chain2.DecodeBytes(data))
|
||||||
|
|
||||||
|
require.Equal(t, chain1.ID, chain2.ID)
|
||||||
|
|
||||||
|
data, err = chain1.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, chain2.UnmarshalJSON(data))
|
||||||
|
|
||||||
|
require.Equal(t, chain1.ID, chain2.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchTypeJson(t *testing.T) {
|
||||||
|
for _, mt := range []MatchType{MatchTypeDenyPriority, MatchTypeFirstMatch, MatchType(100)} {
|
||||||
|
var chain Chain
|
||||||
|
chain.MatchType = mt
|
||||||
|
|
||||||
|
data, err := chain.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
if mt == MatchTypeDenyPriority {
|
||||||
|
require.Equal(t, []byte("{\"ID\":null,\"Rules\":null,\"MatchType\":\"DenyPriority\"}"), data)
|
||||||
|
} else if mt == MatchTypeFirstMatch {
|
||||||
|
require.Equal(t, []byte("{\"ID\":null,\"Rules\":null,\"MatchType\":\"FirstMatch\"}"), data)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, []byte(fmt.Sprintf("{\"ID\":null,\"Rules\":null,\"MatchType\":\"%d\"}", mt)), data)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed Chain
|
||||||
|
require.NoError(t, parsed.UnmarshalJSON(data))
|
||||||
|
require.Equal(t, chain, parsed)
|
||||||
|
|
||||||
|
require.Error(t, parsed.UnmarshalJSON([]byte("{\"ID\":\"\",\"Rules\":null,\"MatchType\":\"NotValid\"}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJsonEnums(t *testing.T) {
|
||||||
|
chain := Chain{
|
||||||
|
ID: []byte("2cca5ae7-cee8-428d-b45f-567fb1d03f01"), // will be encoded to base64
|
||||||
|
MatchType: MatchTypeFirstMatch,
|
||||||
|
Rules: []Rule{
|
||||||
|
{
|
||||||
|
Status: AccessDenied,
|
||||||
|
Actions: Actions{
|
||||||
|
Names: []string{native.MethodDeleteObject, native.MethodGetContainer},
|
||||||
|
},
|
||||||
|
Resources: Resources{
|
||||||
|
Names: []string{native.ResourceFormatAllObjects},
|
||||||
|
},
|
||||||
|
Condition: []Condition{
|
||||||
|
{
|
||||||
|
Op: CondStringEquals,
|
||||||
|
Object: ObjectRequest,
|
||||||
|
Key: native.PropertyKeyActorRole,
|
||||||
|
Value: native.PropertyValueContainerRoleOthers,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Status: QuotaLimitReached,
|
||||||
|
Actions: Actions{
|
||||||
|
Inverted: true,
|
||||||
|
Names: []string{native.MethodPutObject},
|
||||||
|
},
|
||||||
|
Resources: Resources{
|
||||||
|
Names: []string{fmt.Sprintf(native.ResourceFormatRootContainerObjects, "9LPLUFZpEmfidG4n44vi2cjXKXSqWT492tCvLJiJ8W1J")},
|
||||||
|
},
|
||||||
|
Any: true,
|
||||||
|
Condition: []Condition{
|
||||||
|
{
|
||||||
|
Op: CondStringNotLike,
|
||||||
|
Object: ObjectResource,
|
||||||
|
Key: native.PropertyKeyObjectType,
|
||||||
|
Value: "regular",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Status: Status(100),
|
||||||
|
Condition: []Condition{
|
||||||
|
{
|
||||||
|
Op: ConditionType(255),
|
||||||
|
Object: ObjectType(128),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := chain.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var parsed Chain
|
||||||
|
require.NoError(t, parsed.UnmarshalJSON(data))
|
||||||
|
require.Equal(t, chain, parsed)
|
||||||
|
|
||||||
|
expected, err := os.ReadFile("./testdata/test_status_json.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, parsed.UnmarshalJSON(expected))
|
||||||
|
require.Equal(t, chain, parsed)
|
||||||
|
}
|
75
pkg/chain/testdata/test_status_json.json
vendored
Normal file
75
pkg/chain/testdata/test_status_json.json
vendored
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
{
|
||||||
|
"ID": "MmNjYTVhZTctY2VlOC00MjhkLWI0NWYtNTY3ZmIxZDAzZjAx",
|
||||||
|
"Rules": [
|
||||||
|
{
|
||||||
|
"Status": "AccessDenied",
|
||||||
|
"Actions": {
|
||||||
|
"Inverted": false,
|
||||||
|
"Names": [
|
||||||
|
"DeleteObject",
|
||||||
|
"GetContainer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Resources": {
|
||||||
|
"Inverted": false,
|
||||||
|
"Names": [
|
||||||
|
"native:object/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Any": false,
|
||||||
|
"Condition": [
|
||||||
|
{
|
||||||
|
"Op": "StringEquals",
|
||||||
|
"Object": "Request",
|
||||||
|
"Key": "$Actor:role",
|
||||||
|
"Value": "others"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Status": "QuotaLimitReached",
|
||||||
|
"Actions": {
|
||||||
|
"Inverted": true,
|
||||||
|
"Names": [
|
||||||
|
"PutObject"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Resources": {
|
||||||
|
"Inverted": false,
|
||||||
|
"Names": [
|
||||||
|
"native:object//9LPLUFZpEmfidG4n44vi2cjXKXSqWT492tCvLJiJ8W1J/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Any": true,
|
||||||
|
"Condition": [
|
||||||
|
{
|
||||||
|
"Op": "StringNotLike",
|
||||||
|
"Object": "Resource",
|
||||||
|
"Key": "$Object:objectType",
|
||||||
|
"Value": "regular"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Status": "100",
|
||||||
|
"Actions": {
|
||||||
|
"Inverted": false,
|
||||||
|
"Names": null
|
||||||
|
},
|
||||||
|
"Resources": {
|
||||||
|
"Inverted": false,
|
||||||
|
"Names": null
|
||||||
|
},
|
||||||
|
"Any": false,
|
||||||
|
"Condition": [
|
||||||
|
{
|
||||||
|
"Op": "255",
|
||||||
|
"Object": "128",
|
||||||
|
"Key": "",
|
||||||
|
"Value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"MatchType": "FirstMatch"
|
||||||
|
}
|
|
@ -6,18 +6,18 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type defaultChainRouter struct {
|
type defaultChainRouter struct {
|
||||||
morph MorphRuleChainStorage
|
morph MorphRuleChainStorageReader
|
||||||
|
|
||||||
local LocalOverrideStorage
|
local LocalOverrideStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultChainRouter(morph MorphRuleChainStorage) ChainRouter {
|
func NewDefaultChainRouter(morph MorphRuleChainStorageReader) ChainRouter {
|
||||||
return &defaultChainRouter{
|
return &defaultChainRouter{
|
||||||
morph: morph,
|
morph: morph,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultChainRouterWithLocalOverrides(morph MorphRuleChainStorage, local LocalOverrideStorage) ChainRouter {
|
func NewDefaultChainRouterWithLocalOverrides(morph MorphRuleChainStorageReader, local LocalOverrideStorage) ChainRouter {
|
||||||
return &defaultChainRouter{
|
return &defaultChainRouter{
|
||||||
morph: morph,
|
morph: morph,
|
||||||
local: local,
|
local: local,
|
||||||
|
@ -86,23 +86,31 @@ func (dr *defaultChainRouter) matchLocalOverrides(name chain.Name, target Target
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, c := range localOverrides {
|
status, ruleFound = dr.getStatusFromChains(localOverrides, r)
|
||||||
if status, ruleFound = c.Match(r); ruleFound && status != chain.Allow {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dr *defaultChainRouter) matchMorphRuleChains(name chain.Name, target Target, r resource.Request) (status chain.Status, ruleFound bool, err error) {
|
func (dr *defaultChainRouter) matchMorphRuleChains(name chain.Name, target Target, r resource.Request) (status chain.Status, ruleFound bool, err error) {
|
||||||
namespaceChains, err := dr.morph.ListMorphRuleChains(name, target)
|
namespaceChains, err := dr.morph.ListMorphRuleChains(name, target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return chain.NoRuleFound, false, err
|
||||||
}
|
|
||||||
for _, c := range namespaceChains {
|
|
||||||
if status, ruleFound = c.Match(r); ruleFound {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
status, ruleFound = dr.getStatusFromChains(namespaceChains, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dr *defaultChainRouter) getStatusFromChains(chains []*chain.Chain, r resource.Request) (chain.Status, bool) {
|
||||||
|
var allow bool
|
||||||
|
for _, c := range chains {
|
||||||
|
if status, found := c.Match(r); found {
|
||||||
|
if status != chain.Allow {
|
||||||
|
return status, true
|
||||||
|
}
|
||||||
|
allow = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allow {
|
||||||
|
return chain.Allow, true
|
||||||
|
}
|
||||||
|
return chain.NoRuleFound, false
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package inmemory
|
package inmemory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
||||||
|
@ -13,14 +15,16 @@ import (
|
||||||
type targetToChain map[engine.Target][]*chain.Chain
|
type targetToChain map[engine.Target][]*chain.Chain
|
||||||
|
|
||||||
type inmemoryLocalStorage struct {
|
type inmemoryLocalStorage struct {
|
||||||
usedChainID map[chain.ID]struct{}
|
usedChainID map[string]struct{}
|
||||||
nameToResourceChains map[chain.Name]targetToChain
|
nameToResourceChains map[chain.Name]targetToChain
|
||||||
|
guard *sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInmemoryLocalStorage() engine.LocalOverrideStorage {
|
func NewInmemoryLocalStorage() engine.LocalOverrideStorage {
|
||||||
return &inmemoryLocalStorage{
|
return &inmemoryLocalStorage{
|
||||||
usedChainID: map[chain.ID]struct{}{},
|
usedChainID: map[string]struct{}{},
|
||||||
nameToResourceChains: make(map[chain.Name]targetToChain),
|
nameToResourceChains: make(map[chain.Name]targetToChain),
|
||||||
|
guard: &sync.RWMutex{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,20 +36,24 @@ func (s *inmemoryLocalStorage) generateChainID(name chain.Name, target engine.Ta
|
||||||
sid = strings.ReplaceAll(sid, "*", "")
|
sid = strings.ReplaceAll(sid, "*", "")
|
||||||
sid = strings.ReplaceAll(sid, "/", ":")
|
sid = strings.ReplaceAll(sid, "/", ":")
|
||||||
sid = strings.ReplaceAll(sid, "::", ":")
|
sid = strings.ReplaceAll(sid, "::", ":")
|
||||||
id = chain.ID(sid)
|
_, ok := s.usedChainID[sid]
|
||||||
_, ok := s.usedChainID[id]
|
|
||||||
if ok {
|
if ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.usedChainID[id] = struct{}{}
|
s.usedChainID[sid] = struct{}{}
|
||||||
|
|
||||||
|
id = chain.ID(sid)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *inmemoryLocalStorage) AddOverride(name chain.Name, target engine.Target, c *chain.Chain) (chain.ID, error) {
|
func (s *inmemoryLocalStorage) AddOverride(name chain.Name, target engine.Target, c *chain.Chain) (chain.ID, error) {
|
||||||
|
s.guard.Lock()
|
||||||
|
defer s.guard.Unlock()
|
||||||
|
|
||||||
// AddOverride assigns generated chain ID if it has not been assigned.
|
// AddOverride assigns generated chain ID if it has not been assigned.
|
||||||
if c.ID == "" {
|
if len(c.ID) == 0 {
|
||||||
c.ID = s.generateChainID(name, target)
|
c.ID = s.generateChainID(name, target)
|
||||||
}
|
}
|
||||||
if s.nameToResourceChains[name] == nil {
|
if s.nameToResourceChains[name] == nil {
|
||||||
|
@ -53,7 +61,7 @@ func (s *inmemoryLocalStorage) AddOverride(name chain.Name, target engine.Target
|
||||||
}
|
}
|
||||||
rc := s.nameToResourceChains[name]
|
rc := s.nameToResourceChains[name]
|
||||||
for i := range rc[target] {
|
for i := range rc[target] {
|
||||||
if rc[target][i].ID == c.ID {
|
if bytes.Equal(rc[target][i].ID, c.ID) {
|
||||||
rc[target][i] = c
|
rc[target][i] = c
|
||||||
return c.ID, nil
|
return c.ID, nil
|
||||||
}
|
}
|
||||||
|
@ -63,15 +71,21 @@ func (s *inmemoryLocalStorage) AddOverride(name chain.Name, target engine.Target
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *inmemoryLocalStorage) GetOverride(name chain.Name, target engine.Target, chainID chain.ID) (*chain.Chain, error) {
|
func (s *inmemoryLocalStorage) GetOverride(name chain.Name, target engine.Target, chainID chain.ID) (*chain.Chain, error) {
|
||||||
|
s.guard.RLock()
|
||||||
|
defer s.guard.RUnlock()
|
||||||
|
|
||||||
if _, ok := s.nameToResourceChains[name]; !ok {
|
if _, ok := s.nameToResourceChains[name]; !ok {
|
||||||
return nil, engine.ErrChainNameNotFound
|
return nil, engine.ErrChainNameNotFound
|
||||||
}
|
}
|
||||||
|
if target.Name == "" {
|
||||||
|
target.Name = "root"
|
||||||
|
}
|
||||||
chains, ok := s.nameToResourceChains[name][target]
|
chains, ok := s.nameToResourceChains[name][target]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, engine.ErrResourceNotFound
|
return nil, engine.ErrResourceNotFound
|
||||||
}
|
}
|
||||||
for _, c := range chains {
|
for _, c := range chains {
|
||||||
if c.ID == chainID {
|
if bytes.Equal(c.ID, chainID) {
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,15 +93,21 @@ func (s *inmemoryLocalStorage) GetOverride(name chain.Name, target engine.Target
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *inmemoryLocalStorage) RemoveOverride(name chain.Name, target engine.Target, chainID chain.ID) error {
|
func (s *inmemoryLocalStorage) RemoveOverride(name chain.Name, target engine.Target, chainID chain.ID) error {
|
||||||
|
s.guard.Lock()
|
||||||
|
defer s.guard.Unlock()
|
||||||
|
|
||||||
if _, ok := s.nameToResourceChains[name]; !ok {
|
if _, ok := s.nameToResourceChains[name]; !ok {
|
||||||
return engine.ErrChainNameNotFound
|
return engine.ErrChainNameNotFound
|
||||||
}
|
}
|
||||||
|
if target.Name == "" {
|
||||||
|
target.Name = "root"
|
||||||
|
}
|
||||||
chains, ok := s.nameToResourceChains[name][target]
|
chains, ok := s.nameToResourceChains[name][target]
|
||||||
if !ok {
|
if !ok {
|
||||||
return engine.ErrResourceNotFound
|
return engine.ErrResourceNotFound
|
||||||
}
|
}
|
||||||
for i, c := range chains {
|
for i, c := range chains {
|
||||||
if c.ID == chainID {
|
if bytes.Equal(c.ID, chainID) {
|
||||||
s.nameToResourceChains[name][target] = append(chains[:i], chains[i+1:]...)
|
s.nameToResourceChains[name][target] = append(chains[:i], chains[i+1:]...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -96,10 +116,16 @@ func (s *inmemoryLocalStorage) RemoveOverride(name chain.Name, target engine.Tar
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *inmemoryLocalStorage) ListOverrides(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
|
func (s *inmemoryLocalStorage) ListOverrides(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
|
||||||
|
s.guard.RLock()
|
||||||
|
defer s.guard.RUnlock()
|
||||||
|
|
||||||
rcs, ok := s.nameToResourceChains[name]
|
rcs, ok := s.nameToResourceChains[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return []*chain.Chain{}, nil
|
return []*chain.Chain{}, nil
|
||||||
}
|
}
|
||||||
|
if target.Name == "" {
|
||||||
|
target.Name = "root"
|
||||||
|
}
|
||||||
for t, chains := range rcs {
|
for t, chains := range rcs {
|
||||||
if t.Type != target.Type {
|
if t.Type != target.Type {
|
||||||
continue
|
continue
|
||||||
|
@ -113,6 +139,20 @@ func (s *inmemoryLocalStorage) ListOverrides(name chain.Name, target engine.Targ
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *inmemoryLocalStorage) DropAllOverrides(name chain.Name) error {
|
func (s *inmemoryLocalStorage) DropAllOverrides(name chain.Name) error {
|
||||||
|
s.guard.Lock()
|
||||||
|
defer s.guard.Unlock()
|
||||||
|
|
||||||
s.nameToResourceChains[name] = make(targetToChain)
|
s.nameToResourceChains[name] = make(targetToChain)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *inmemoryLocalStorage) ListOverrideDefinedTargets(name chain.Name) ([]engine.Target, error) {
|
||||||
|
s.guard.RLock()
|
||||||
|
defer s.guard.RUnlock()
|
||||||
|
ttc := s.nameToResourceChains[name]
|
||||||
|
var keys []engine.Target
|
||||||
|
for k := range ttc {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
|
@ -14,9 +14,7 @@ const (
|
||||||
nonExistChainId = "ingress:LxGyWyL"
|
nonExistChainId = "ingress:LxGyWyL"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var resrc = engine.ContainerTarget(container)
|
||||||
resrc = engine.ContainerTarget(container)
|
|
||||||
)
|
|
||||||
|
|
||||||
func testInmemLocalStorage() *inmemoryLocalStorage {
|
func testInmemLocalStorage() *inmemoryLocalStorage {
|
||||||
return NewInmemoryLocalStorage().(*inmemoryLocalStorage)
|
return NewInmemoryLocalStorage().(*inmemoryLocalStorage)
|
||||||
|
@ -186,6 +184,9 @@ func TestListOverrides(t *testing.T) {
|
||||||
inmem.AddOverride(chain.Ingress, resrc, addChain)
|
inmem.AddOverride(chain.Ingress, resrc, addChain)
|
||||||
l, _ := inmem.ListOverrides(chain.Ingress, resrc)
|
l, _ := inmem.ListOverrides(chain.Ingress, resrc)
|
||||||
require.Len(t, l, 1)
|
require.Len(t, l, 1)
|
||||||
|
targets, err := inmem.ListOverrideDefinedTargets(chain.Ingress)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, []engine.Target{resrc}, targets)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("list after drop", func(t *testing.T) {
|
t.Run("list after drop", func(t *testing.T) {
|
||||||
|
@ -210,12 +211,12 @@ func TestGenerateID(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasDuplicates(ids []chain.ID) bool {
|
func hasDuplicates(ids []chain.ID) bool {
|
||||||
seen := make(map[chain.ID]bool)
|
seen := make(map[string]bool)
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
if seen[id] {
|
if seen[string(id)] {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
seen[id] = true
|
seen[string(id)] = true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,3 +52,11 @@ func (s *inmemoryMorphRuleChainStorage) ListMorphRuleChains(name chain.Name, tar
|
||||||
}
|
}
|
||||||
return nil, engine.ErrUnknownTarget
|
return nil, engine.ErrUnknownTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *inmemoryMorphRuleChainStorage) GetAdmin() (util.Uint160, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inmemoryMorphRuleChainStorage) SetAdmin(_ util.Uint160) (util.Uint256, uint32, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,8 @@ type LocalOverrideStorage interface {
|
||||||
ListOverrides(name chain.Name, target Target) ([]*chain.Chain, error)
|
ListOverrides(name chain.Name, target Target) ([]*chain.Chain, error)
|
||||||
|
|
||||||
DropAllOverrides(name chain.Name) error
|
DropAllOverrides(name chain.Name) error
|
||||||
|
|
||||||
|
ListOverrideDefinedTargets(name chain.Name) ([]Target, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TargetType rune
|
type TargetType rune
|
||||||
|
@ -68,12 +70,12 @@ func NewRequestTarget(namespace, container string) RequestTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rt *RequestTarget) Targets() (targets []Target) {
|
func (rt *RequestTarget) Targets() (targets []Target) {
|
||||||
if rt.Container != nil {
|
|
||||||
targets = append(targets, *rt.Container)
|
|
||||||
}
|
|
||||||
if rt.Namespace != nil {
|
if rt.Namespace != nil {
|
||||||
targets = append(targets, *rt.Namespace)
|
targets = append(targets, *rt.Namespace)
|
||||||
}
|
}
|
||||||
|
if rt.Container != nil {
|
||||||
|
targets = append(targets, *rt.Container)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,17 +93,27 @@ func ContainerTarget(container string) Target {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MorphRuleChainStorage is the interface to manage chains from the chain storage.
|
// MorphRuleChainStorageReader is the interface that provides read-only methods to receive
|
||||||
|
// data like chains, target or admin from a chain storage.
|
||||||
|
type MorphRuleChainStorageReader interface {
|
||||||
|
// ListMorphRuleChains just lists deserialized chains.
|
||||||
|
ListMorphRuleChains(name chain.Name, target Target) ([]*chain.Chain, error)
|
||||||
|
|
||||||
|
GetAdmin() (util.Uint160, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MorphRuleChainStorage is the interface to read and manage data within a chain storage.
|
||||||
// Basically, this implies that the storage manages rules stored in policy contract.
|
// Basically, this implies that the storage manages rules stored in policy contract.
|
||||||
type MorphRuleChainStorage interface {
|
type MorphRuleChainStorage interface {
|
||||||
|
MorphRuleChainStorageReader
|
||||||
|
|
||||||
// AddMorphRuleChain adds a chain rule to the policy contract and returns transaction hash, VUB and error.
|
// AddMorphRuleChain adds a chain rule to the policy contract and returns transaction hash, VUB and error.
|
||||||
AddMorphRuleChain(name chain.Name, target Target, c *chain.Chain) (util.Uint256, uint32, error)
|
AddMorphRuleChain(name chain.Name, target Target, c *chain.Chain) (util.Uint256, uint32, error)
|
||||||
|
|
||||||
// RemoveMorphRuleChain removes a chain rule to the policy contract and returns transaction hash, VUB and error.
|
// RemoveMorphRuleChain removes a chain rule to the policy contract and returns transaction hash, VUB and error.
|
||||||
RemoveMorphRuleChain(name chain.Name, target Target, chainID chain.ID) (util.Uint256, uint32, error)
|
RemoveMorphRuleChain(name chain.Name, target Target, chainID chain.ID) (util.Uint256, uint32, error)
|
||||||
|
|
||||||
// ListMorphRuleChains just lists deserialized chains.
|
SetAdmin(addr util.Uint160) (util.Uint256, uint32, error)
|
||||||
ListMorphRuleChains(name chain.Name, target Target) ([]*chain.Chain, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Engine is the interface that provides methods to check request permissions checking
|
// Engine is the interface that provides methods to check request permissions checking
|
||||||
|
|
267
pkg/marshal/marshal.go
Normal file
267
pkg/marshal/marshal.go
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
package marshal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Version byte = 0 // increase if breaking change
|
||||||
|
|
||||||
|
ByteSize int = 1
|
||||||
|
UInt8Size int = ByteSize
|
||||||
|
BoolSize int = ByteSize
|
||||||
|
|
||||||
|
nilSlice int64 = -1
|
||||||
|
nilSliceSize int = 1
|
||||||
|
|
||||||
|
byteTrue uint8 = 1
|
||||||
|
byteFalse uint8 = 0
|
||||||
|
|
||||||
|
// maxSliceLen taken from https://github.com/neo-project/neo/blob/38218bbee5bbe8b33cd8f9453465a19381c9a547/src/Neo/IO/Helper.cs#L77
|
||||||
|
maxSliceLen = 0x1000000
|
||||||
|
)
|
||||||
|
|
||||||
|
type MarshallerError struct {
|
||||||
|
errMsg string
|
||||||
|
offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MarshallerError) Error() string {
|
||||||
|
if e == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if e.offset < 0 {
|
||||||
|
return e.errMsg
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s (offset: %d)", e.errMsg, e.offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errBufTooSmall(t string, marshal bool, offset int) error {
|
||||||
|
action := "unmarshal"
|
||||||
|
if marshal {
|
||||||
|
action = "marshal"
|
||||||
|
}
|
||||||
|
return &MarshallerError{
|
||||||
|
errMsg: fmt.Sprintf("not enough bytes left to %s value of type '%s'", action, t),
|
||||||
|
offset: offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyMarshal(buf []byte, lastOffset int) error {
|
||||||
|
if len(buf) != lastOffset {
|
||||||
|
return &MarshallerError{
|
||||||
|
errMsg: "actual data size differs from expected",
|
||||||
|
offset: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyUnmarshal(buf []byte, lastOffset int) error {
|
||||||
|
if len(buf) != lastOffset {
|
||||||
|
return &MarshallerError{
|
||||||
|
errMsg: "unmarshalled bytes left",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SliceSize[T any](slice []T, sizeOf func(T) int) int {
|
||||||
|
if slice == nil {
|
||||||
|
return nilSliceSize
|
||||||
|
}
|
||||||
|
s := Int64Size(int64(len(slice)))
|
||||||
|
for _, v := range slice {
|
||||||
|
s += sizeOf(v)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func SliceMarshal[T any](buf []byte, offset int, slice []T, marshalT func([]byte, int, T) (int, error)) (int, error) {
|
||||||
|
if slice == nil {
|
||||||
|
return Int64Marshal(buf, offset, nilSlice)
|
||||||
|
}
|
||||||
|
if len(slice) > maxSliceLen {
|
||||||
|
return 0, &MarshallerError{
|
||||||
|
errMsg: fmt.Sprintf("slice size if too big: '%d'", len(slice)),
|
||||||
|
offset: offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset, err := Int64Marshal(buf, offset, int64(len(slice)))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
for _, v := range slice {
|
||||||
|
offset, err = marshalT(buf, offset, v)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SliceUnmarshal[T any](buf []byte, offset int, unmarshalT func(buf []byte, offset int) (T, int, error)) ([]T, int, error) {
|
||||||
|
size, offset, err := Int64Unmarshal(buf, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if size == nilSlice {
|
||||||
|
return nil, offset, nil
|
||||||
|
}
|
||||||
|
if size > maxSliceLen {
|
||||||
|
return nil, 0, &MarshallerError{
|
||||||
|
errMsg: fmt.Sprintf("slice size if too big: '%d'", size),
|
||||||
|
offset: offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if size < 0 {
|
||||||
|
return nil, 0, &MarshallerError{
|
||||||
|
errMsg: fmt.Sprintf("invalid slice size: '%d'", size),
|
||||||
|
offset: offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := make([]T, size)
|
||||||
|
for idx := 0; idx < len(result); idx++ {
|
||||||
|
result[idx], offset, err = unmarshalT(buf, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int64Size(v int64) int {
|
||||||
|
// https://cs.opensource.google/go/go/+/master:src/encoding/binary/varint.go;l=92;drc=dac9b9ddbd5160c5f4552410f5f8281bd5eed38c
|
||||||
|
// and
|
||||||
|
// https://cs.opensource.google/go/go/+/master:src/encoding/binary/varint.go;l=41;drc=dac9b9ddbd5160c5f4552410f5f8281bd5eed38c
|
||||||
|
ux := uint64(v) << 1
|
||||||
|
if v < 0 {
|
||||||
|
ux = ^ux
|
||||||
|
}
|
||||||
|
s := 0
|
||||||
|
for ux >= 0x80 {
|
||||||
|
s++
|
||||||
|
ux >>= 7
|
||||||
|
}
|
||||||
|
return s + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int64Marshal(buf []byte, offset int, v int64) (int, error) {
|
||||||
|
if len(buf)-offset < Int64Size(v) {
|
||||||
|
return 0, errBufTooSmall("int64", true, offset)
|
||||||
|
}
|
||||||
|
return offset + binary.PutVarint(buf[offset:], v), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int64Unmarshal(buf []byte, offset int) (int64, int, error) {
|
||||||
|
v, read := binary.Varint(buf[offset:])
|
||||||
|
if read == 0 {
|
||||||
|
return 0, 0, errBufTooSmall("int64", false, offset)
|
||||||
|
}
|
||||||
|
if read < 0 {
|
||||||
|
return 0, 0, &MarshallerError{
|
||||||
|
errMsg: "int64 unmarshal overflow",
|
||||||
|
offset: offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v, offset + read, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringSize(s string) int {
|
||||||
|
return Int64Size(int64(len(s))) + len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringMarshal(buf []byte, offset int, s string) (int, error) {
|
||||||
|
if len(s) > maxSliceLen {
|
||||||
|
return 0, &MarshallerError{
|
||||||
|
errMsg: fmt.Sprintf("string is too long: '%d'", len(s)),
|
||||||
|
offset: offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(buf)-offset < Int64Size(int64(len(s)))+len(s) {
|
||||||
|
return 0, errBufTooSmall("string", true, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
offset, err := Int64Marshal(buf, offset, int64(len(s)))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
return offset, nil
|
||||||
|
}
|
||||||
|
return offset + copy(buf[offset:], s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringUnmarshal(buf []byte, offset int) (string, int, error) {
|
||||||
|
size, offset, err := Int64Unmarshal(buf, offset)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
if size == 0 {
|
||||||
|
return "", offset, nil
|
||||||
|
}
|
||||||
|
if size > maxSliceLen {
|
||||||
|
return "", 0, &MarshallerError{
|
||||||
|
errMsg: fmt.Sprintf("string is too long: '%d'", size),
|
||||||
|
offset: offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if size < 0 {
|
||||||
|
return "", 0, &MarshallerError{
|
||||||
|
errMsg: fmt.Sprintf("invalid string size: '%d'", size),
|
||||||
|
offset: offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(buf)-offset < int(size) {
|
||||||
|
return "", 0, errBufTooSmall("string", false, offset)
|
||||||
|
}
|
||||||
|
return string(buf[offset : offset+int(size)]), offset + int(size), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UInt8Marshal(buf []byte, offset int, value uint8) (int, error) {
|
||||||
|
if len(buf)-offset < 1 {
|
||||||
|
return 0, errBufTooSmall("uint8", true, offset)
|
||||||
|
}
|
||||||
|
buf[offset] = value
|
||||||
|
return offset + 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UInt8Unmarshal(buf []byte, offset int) (uint8, int, error) {
|
||||||
|
if len(buf)-offset < 1 {
|
||||||
|
return 0, 0, errBufTooSmall("uint8", false, offset)
|
||||||
|
}
|
||||||
|
return buf[offset], offset + 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ByteMarshal(buf []byte, offset int, value byte) (int, error) {
|
||||||
|
return UInt8Marshal(buf, offset, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ByteUnmarshal(buf []byte, offset int) (byte, int, error) {
|
||||||
|
return UInt8Unmarshal(buf, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BoolMarshal(buf []byte, offset int, value bool) (int, error) {
|
||||||
|
if value {
|
||||||
|
return UInt8Marshal(buf, offset, byteTrue)
|
||||||
|
}
|
||||||
|
return UInt8Marshal(buf, offset, byteFalse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BoolUnmarshal(buf []byte, offset int) (bool, int, error) {
|
||||||
|
v, offset, err := UInt8Unmarshal(buf, offset)
|
||||||
|
if err != nil {
|
||||||
|
return false, 0, err
|
||||||
|
}
|
||||||
|
if v == byteTrue {
|
||||||
|
return true, offset, nil
|
||||||
|
}
|
||||||
|
if v == byteFalse {
|
||||||
|
return false, offset, nil
|
||||||
|
}
|
||||||
|
return false, 0, &MarshallerError{
|
||||||
|
errMsg: fmt.Sprintf("invalid marshalled value for bool: %d", v),
|
||||||
|
offset: offset - BoolSize,
|
||||||
|
}
|
||||||
|
}
|
313
pkg/marshal/marshal_test.go
Normal file
313
pkg/marshal/marshal_test.go
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
package marshal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarshalling(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
t.Run("slice", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
t.Run("nil slice", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var int64s []int64
|
||||||
|
expectedSize := SliceSize(int64s, Int64Size)
|
||||||
|
require.Equal(t, 1, expectedSize)
|
||||||
|
buf := make([]byte, expectedSize)
|
||||||
|
offset, err := SliceMarshal(buf, 0, int64s, Int64Marshal)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyMarshal(buf, offset))
|
||||||
|
|
||||||
|
result, offset, err := SliceUnmarshal(buf, 0, Int64Unmarshal)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyUnmarshal(buf, offset))
|
||||||
|
require.Nil(t, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty slice", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
int64s := make([]int64, 0)
|
||||||
|
expectedSize := SliceSize(int64s, Int64Size)
|
||||||
|
require.Equal(t, 1, expectedSize)
|
||||||
|
buf := make([]byte, expectedSize)
|
||||||
|
offset, err := SliceMarshal(buf, 0, int64s, Int64Marshal)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyMarshal(buf, offset))
|
||||||
|
|
||||||
|
result, offset, err := SliceUnmarshal(buf, 0, Int64Unmarshal)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyUnmarshal(buf, offset))
|
||||||
|
require.NotNil(t, result)
|
||||||
|
require.Len(t, result, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non empty slice", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
int64s := make([]int64, 100)
|
||||||
|
for i := range int64s {
|
||||||
|
int64s[i] = int64(i)
|
||||||
|
}
|
||||||
|
expectedSize := SliceSize(int64s, Int64Size)
|
||||||
|
buf := make([]byte, expectedSize)
|
||||||
|
offset, err := SliceMarshal(buf, 0, int64s, Int64Marshal)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyMarshal(buf, offset))
|
||||||
|
|
||||||
|
result, offset, err := SliceUnmarshal(buf, 0, Int64Unmarshal)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyUnmarshal(buf, offset))
|
||||||
|
require.Equal(t, int64s, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("corrupted slice size", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
int64s := make([]int64, 100)
|
||||||
|
for i := range int64s {
|
||||||
|
int64s[i] = int64(i)
|
||||||
|
}
|
||||||
|
expectedSize := SliceSize(int64s, Int64Size)
|
||||||
|
buf := make([]byte, expectedSize)
|
||||||
|
offset, err := SliceMarshal(buf, 0, int64s, Int64Marshal)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyMarshal(buf, offset))
|
||||||
|
|
||||||
|
for i := 0; i < binary.MaxVarintLen64; i++ {
|
||||||
|
buf[i] = 129
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = SliceUnmarshal(buf, 0, Int64Unmarshal)
|
||||||
|
var mErr *MarshallerError
|
||||||
|
require.ErrorAs(t, err, &mErr)
|
||||||
|
|
||||||
|
for i := 0; i < binary.MaxVarintLen64; i++ {
|
||||||
|
buf[i] = 127
|
||||||
|
}
|
||||||
|
_, _, err = SliceUnmarshal(buf, 0, Int64Unmarshal)
|
||||||
|
require.ErrorAs(t, err, &mErr)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("corrupted slice item", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
int64s := make([]int64, 100)
|
||||||
|
for i := range int64s {
|
||||||
|
int64s[i] = int64(i)
|
||||||
|
}
|
||||||
|
expectedSize := SliceSize(int64s, Int64Size)
|
||||||
|
buf := make([]byte, expectedSize)
|
||||||
|
offset, err := SliceMarshal(buf, 0, int64s, Int64Marshal)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyMarshal(buf, offset))
|
||||||
|
|
||||||
|
for i := 2; i < binary.MaxVarintLen64+2; i++ {
|
||||||
|
buf[i] = 129
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = SliceUnmarshal(buf, 0, Int64Unmarshal)
|
||||||
|
var mErr *MarshallerError
|
||||||
|
require.ErrorAs(t, err, &mErr)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("small buffer", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
int64s := make([]int64, 100)
|
||||||
|
for i := range int64s {
|
||||||
|
int64s[i] = int64(i)
|
||||||
|
}
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
_, err := SliceMarshal(buf, 0, int64s, Int64Marshal)
|
||||||
|
var mErr *MarshallerError
|
||||||
|
require.ErrorAs(t, err, &mErr)
|
||||||
|
|
||||||
|
buf = make([]byte, 10)
|
||||||
|
_, err = SliceMarshal(buf, 0, int64s, Int64Marshal)
|
||||||
|
require.ErrorAs(t, err, &mErr)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("int64", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
require.Equal(t, 1, Int64Size(0))
|
||||||
|
require.Equal(t, binary.MaxVarintLen64, Int64Size(math.MaxInt64))
|
||||||
|
require.Equal(t, binary.MaxVarintLen64, Int64Size(math.MinInt64))
|
||||||
|
|
||||||
|
for _, v := range []int64{0, math.MinInt64, math.MaxInt64} {
|
||||||
|
size := Int64Size(v)
|
||||||
|
buf := make([]byte, size)
|
||||||
|
offset, err := Int64Marshal(buf, 0, v)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyMarshal(buf, offset))
|
||||||
|
|
||||||
|
uv, offset, err := Int64Unmarshal(buf, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyUnmarshal(buf, offset))
|
||||||
|
require.Equal(t, v, uv)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid buffer", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var mErr *MarshallerError
|
||||||
|
|
||||||
|
_, err := Int64Marshal([]byte{}, 0, 100500)
|
||||||
|
require.ErrorAs(t, err, &mErr)
|
||||||
|
|
||||||
|
_, _, err = Int64Unmarshal(nil, 0)
|
||||||
|
require.ErrorAs(t, err, &mErr)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("overflow", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var mErr *MarshallerError
|
||||||
|
|
||||||
|
var v int64 = math.MaxInt64
|
||||||
|
buf := make([]byte, Int64Size(v))
|
||||||
|
_, err := Int64Marshal(buf, 0, v)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
buf[9] = 2
|
||||||
|
|
||||||
|
_, _, err = Int64Unmarshal(buf, 0)
|
||||||
|
require.ErrorAs(t, err, &mErr)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
for _, v := range []string{
|
||||||
|
"", "arn:aws:iam::namespace:group/some_group", "$Object:homomorphicHash",
|
||||||
|
"native:container/ns/9LPLUFZpEmfidG4n44vi2cjXKXSqWT492tCvLJiJ8W1J",
|
||||||
|
} {
|
||||||
|
size := StringSize(v)
|
||||||
|
buf := make([]byte, size)
|
||||||
|
offset, err := StringMarshal(buf, 0, v)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyMarshal(buf, offset))
|
||||||
|
|
||||||
|
uv, offset, err := StringUnmarshal(buf, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyUnmarshal(buf, offset))
|
||||||
|
require.Equal(t, v, uv)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid buffer", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
str := "avada kedavra"
|
||||||
|
|
||||||
|
var mErr *MarshallerError
|
||||||
|
_, err := StringMarshal(nil, 0, str)
|
||||||
|
require.ErrorAs(t, err, &mErr)
|
||||||
|
|
||||||
|
_, _, err = StringUnmarshal(nil, 0)
|
||||||
|
require.ErrorAs(t, err, &mErr)
|
||||||
|
|
||||||
|
buf := make([]byte, StringSize(str))
|
||||||
|
offset, err := StringMarshal(buf, 0, str)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyMarshal(buf, offset))
|
||||||
|
buf = buf[:len(buf)-1]
|
||||||
|
_, _, err = StringUnmarshal(buf, 0)
|
||||||
|
require.ErrorAs(t, err, &mErr)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("uint8, byte", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, v := range []byte{0, 8, 16, 32, 64, 128, 255} {
|
||||||
|
buf := make([]byte, ByteSize)
|
||||||
|
offset, err := ByteMarshal(buf, 0, v)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyMarshal(buf, offset))
|
||||||
|
|
||||||
|
ub, offset, err := ByteUnmarshal(buf, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyUnmarshal(buf, offset))
|
||||||
|
require.Equal(t, v, ub)
|
||||||
|
|
||||||
|
buf = make([]byte, UInt8Size)
|
||||||
|
offset, err = UInt8Marshal(buf, 0, v)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyMarshal(buf, offset))
|
||||||
|
|
||||||
|
uu, offset, err := UInt8Unmarshal(buf, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyUnmarshal(buf, offset))
|
||||||
|
require.Equal(t, v, uu)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bool", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
for _, v := range []bool{false, true} {
|
||||||
|
buf := make([]byte, BoolSize)
|
||||||
|
offset, err := BoolMarshal(buf, 0, v)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyMarshal(buf, offset))
|
||||||
|
|
||||||
|
ub, offset, err := BoolUnmarshal(buf, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyUnmarshal(buf, offset))
|
||||||
|
require.Equal(t, v, ub)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid value", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
buf := make([]byte, BoolSize)
|
||||||
|
offset, err := BoolMarshal(buf, 0, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, VerifyMarshal(buf, offset))
|
||||||
|
|
||||||
|
buf[0] = 2
|
||||||
|
|
||||||
|
_, _, err = BoolUnmarshal(buf, 0)
|
||||||
|
var mErr *MarshallerError
|
||||||
|
require.ErrorAs(t, err, &mErr)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid buffer", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
var mErr *MarshallerError
|
||||||
|
|
||||||
|
_, err := BoolMarshal(nil, 0, true)
|
||||||
|
require.ErrorAs(t, err, &mErr)
|
||||||
|
|
||||||
|
buf := append(make([]byte, BoolSize), 100)
|
||||||
|
offset, err := BoolMarshal(buf, 0, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.ErrorAs(t, VerifyMarshal(buf, offset), &mErr)
|
||||||
|
|
||||||
|
v, offset, err := BoolUnmarshal(buf, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, v)
|
||||||
|
require.ErrorAs(t, VerifyUnmarshal(buf, offset), &mErr)
|
||||||
|
|
||||||
|
_, _, err = BoolUnmarshal(nil, 0)
|
||||||
|
require.ErrorAs(t, err, &mErr)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ import (
|
||||||
client "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/policy"
|
client "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/policy"
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
||||||
"github.com/mr-tron/base58"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
@ -23,13 +22,20 @@ var (
|
||||||
ErrEngineTargetTypeUnsupported = errors.New("this target type is not supported yet")
|
ErrEngineTargetTypeUnsupported = errors.New("this target type is not supported yet")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ContractStorage is the interface to manage chain rules within the policy contract.
|
// ContractStorage is the interface to manage chain rules within Policy contract.
|
||||||
type ContractStorage struct {
|
type ContractStorage struct {
|
||||||
contractInterface *client.Contract
|
contractInterface *client.Contract
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ engine.MorphRuleChainStorage = (*ContractStorage)(nil)
|
var _ engine.MorphRuleChainStorage = (*ContractStorage)(nil)
|
||||||
|
|
||||||
|
// ContractStorageReader is the interface to read data from Policy contract.
|
||||||
|
type ContractStorageReader struct {
|
||||||
|
contractReaderInterface *client.ContractReader
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ engine.MorphRuleChainStorageReader = (*ContractStorageReader)(nil)
|
||||||
|
|
||||||
func NewContractStorage(actor client.Actor, contract util.Uint160) *ContractStorage {
|
func NewContractStorage(actor client.Actor, contract util.Uint160) *ContractStorage {
|
||||||
return &ContractStorage{
|
return &ContractStorage{
|
||||||
contractInterface: client.New(actor, contract),
|
contractInterface: client.New(actor, contract),
|
||||||
|
@ -44,18 +50,8 @@ func NewContractStorageWithSimpleActor(rpcActor actor.RPCActor, acc *wallet.Acco
|
||||||
return NewContractStorage(act, contract), nil
|
return NewContractStorage(act, contract), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformNameIfContainer(target engine.Target) (name string) {
|
|
||||||
name = target.Name
|
|
||||||
if target.Type == engine.Container {
|
|
||||||
// Container name can be too long and, thus, cannot be
|
|
||||||
// used as a key name for policy-contract storage.
|
|
||||||
name = base58.FastBase58Encoding([]byte(target.Name))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ContractStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (txHash util.Uint256, vub uint32, err error) {
|
func (s *ContractStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (txHash util.Uint256, vub uint32, err error) {
|
||||||
if c.ID == "" {
|
if len(c.ID) == 0 {
|
||||||
err = ErrEmptyChainID
|
err = ErrEmptyChainID
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -66,14 +62,13 @@ func (s *ContractStorage) AddMorphRuleChain(name chain.Name, target engine.Targe
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fullName := prefixedChainName(name, c.ID)
|
fullName := prefixedChainName(name, c.ID)
|
||||||
targetName := transformNameIfContainer(target)
|
|
||||||
|
|
||||||
txHash, vub, err = s.contractInterface.AddChain(big.NewInt(int64(kind)), targetName, fullName, c.Bytes())
|
txHash, vub, err = s.contractInterface.AddChain(big.NewInt(int64(kind)), target.Name, fullName, c.Bytes())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ContractStorage) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (txHash util.Uint256, vub uint32, err error) {
|
func (s *ContractStorage) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (txHash util.Uint256, vub uint32, err error) {
|
||||||
if chainID == "" {
|
if len(chainID) == 0 {
|
||||||
err = ErrEmptyChainID
|
err = ErrEmptyChainID
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -84,9 +79,8 @@ func (s *ContractStorage) RemoveMorphRuleChain(name chain.Name, target engine.Ta
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fullName := prefixedChainName(name, chainID)
|
fullName := prefixedChainName(name, chainID)
|
||||||
targetName := transformNameIfContainer(target)
|
|
||||||
|
|
||||||
txHash, vub, err = s.contractInterface.RemoveChain(big.NewInt(int64(kind)), targetName, fullName)
|
txHash, vub, err = s.contractInterface.RemoveChain(big.NewInt(int64(kind)), target.Name, fullName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,9 +89,8 @@ func (s *ContractStorage) ListMorphRuleChains(name chain.Name, target engine.Tar
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
targetName := transformNameIfContainer(target)
|
|
||||||
|
|
||||||
items, err := s.contractInterface.ListChainsByPrefix(big.NewInt(int64(kind)), targetName, []byte(name))
|
items, err := s.contractInterface.ListChainsByPrefix(big.NewInt(int64(kind)), target.Name, []byte(name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -118,16 +111,55 @@ func (s *ContractStorage) ListMorphRuleChains(name chain.Name, target engine.Tar
|
||||||
return chains, nil
|
return chains, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ContractStorage) GetAdmin() (util.Uint160, error) {
|
||||||
|
return s.contractInterface.GetAdmin()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContractStorage) SetAdmin(addr util.Uint160) (util.Uint256, uint32, error) {
|
||||||
|
return s.contractInterface.SetAdmin(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContractStorageReader(inv client.Invoker, contract util.Uint160) *ContractStorageReader {
|
||||||
|
return &ContractStorageReader{
|
||||||
|
contractReaderInterface: client.NewReader(inv, contract),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContractStorageReader) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
|
||||||
|
kind, err := policyKind(target.Type)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := s.contractReaderInterface.ListChainsByPrefix(big.NewInt(int64(kind)), target.Name, []byte(name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var chains []*chain.Chain
|
||||||
|
for _, item := range items {
|
||||||
|
serialized, err := bytesFromStackItem(item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c := new(chain.Chain)
|
||||||
|
if err := c.DecodeBytes(serialized); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
chains = append(chains, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chains, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContractStorageReader) GetAdmin() (util.Uint160, error) {
|
||||||
|
return s.contractReaderInterface.GetAdmin()
|
||||||
|
}
|
||||||
|
|
||||||
func bytesFromStackItem(param stackitem.Item) ([]byte, error) {
|
func bytesFromStackItem(param stackitem.Item) ([]byte, error) {
|
||||||
switch param.Type() {
|
switch param.Type() {
|
||||||
case stackitem.BufferT, stackitem.ByteArrayT:
|
case stackitem.BufferT, stackitem.ByteArrayT, stackitem.IntegerT:
|
||||||
return param.TryBytes()
|
return param.TryBytes()
|
||||||
case stackitem.IntegerT:
|
|
||||||
n, err := param.TryInteger()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't parse integer bytes: %w", err)
|
|
||||||
}
|
|
||||||
return n.Bytes(), nil
|
|
||||||
case stackitem.AnyT:
|
case stackitem.AnyT:
|
||||||
if param.Value() == nil {
|
if param.Value() == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
5
schema/common/consts.go
Normal file
5
schema/common/consts.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
const (
|
||||||
|
PropertyKeyFrostFSIDGroupID = "frostfsid:groupID"
|
||||||
|
)
|
|
@ -9,6 +9,16 @@ const (
|
||||||
MethodRangeObject = "RangeObject"
|
MethodRangeObject = "RangeObject"
|
||||||
MethodHashObject = "HashObject"
|
MethodHashObject = "HashObject"
|
||||||
|
|
||||||
|
MethodPutContainer = "PutContainer"
|
||||||
|
MethodDeleteContainer = "DeleteContainer"
|
||||||
|
MethodGetContainer = "GetContainer"
|
||||||
|
MethodListContainers = "ListContainers"
|
||||||
|
MethodSetContainerEACL = "SetContainerEACL"
|
||||||
|
MethodGetContainerEACL = "GetContainerEACL"
|
||||||
|
|
||||||
|
ObjectPrefix = "native:object"
|
||||||
|
ContainerPrefix = "native:container"
|
||||||
|
|
||||||
ResourceFormatNamespaceObjects = "native:object/%s/*"
|
ResourceFormatNamespaceObjects = "native:object/%s/*"
|
||||||
ResourceFormatNamespaceContainerObjects = "native:object/%s/%s/*"
|
ResourceFormatNamespaceContainerObjects = "native:object/%s/%s/*"
|
||||||
ResourceFormatNamespaceContainerObject = "native:object/%s/%s/%s"
|
ResourceFormatNamespaceContainerObject = "native:object/%s/%s/%s"
|
||||||
|
@ -27,8 +37,9 @@ const (
|
||||||
|
|
||||||
ResourceFormatAllContainers = "native:container/*"
|
ResourceFormatAllContainers = "native:container/*"
|
||||||
|
|
||||||
PropertyKeyActorPublicKey = "$Actor:publicKey"
|
PropertyKeyActorPublicKey = "$Actor:publicKey"
|
||||||
PropertyKeyActorRole = "$Actor:role"
|
PropertyKeyActorRole = "$Actor:role"
|
||||||
|
|
||||||
PropertyKeyObjectVersion = "$Object:version"
|
PropertyKeyObjectVersion = "$Object:version"
|
||||||
PropertyKeyObjectID = "$Object:objectID"
|
PropertyKeyObjectID = "$Object:objectID"
|
||||||
PropertyKeyObjectContainerID = "$Object:containerID"
|
PropertyKeyObjectContainerID = "$Object:containerID"
|
||||||
|
@ -38,4 +49,11 @@ const (
|
||||||
PropertyKeyObjectPayloadHash = "$Object:payloadHash"
|
PropertyKeyObjectPayloadHash = "$Object:payloadHash"
|
||||||
PropertyKeyObjectType = "$Object:objectType"
|
PropertyKeyObjectType = "$Object:objectType"
|
||||||
PropertyKeyObjectHomomorphicHash = "$Object:homomorphicHash"
|
PropertyKeyObjectHomomorphicHash = "$Object:homomorphicHash"
|
||||||
|
|
||||||
|
PropertyKeyContainerOwnerID = "$Container:ownerID"
|
||||||
|
|
||||||
|
PropertyValueContainerRoleOwner = "owner"
|
||||||
|
PropertyValueContainerRoleIR = "ir"
|
||||||
|
PropertyValueContainerRoleContainer = "container"
|
||||||
|
PropertyValueContainerRoleOthers = "others"
|
||||||
)
|
)
|
||||||
|
|
45
schema/native/util/validation.go
Normal file
45
schema/native/util/validation.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
||||||
|
)
|
||||||
|
|
||||||
|
var nativePatterns = []string{
|
||||||
|
native.ResourceFormatNamespaceObjects, native.ResourceFormatNamespaceContainerObjects,
|
||||||
|
native.ResourceFormatNamespaceContainerObject, native.ResourceFormatRootObjects,
|
||||||
|
native.ResourceFormatRootContainerObjects, native.ResourceFormatRootContainerObject,
|
||||||
|
native.ResourceFormatAllObjects, native.ResourceFormatNamespaceContainer,
|
||||||
|
native.ResourceFormatNamespaceContainers, native.ResourceFormatRootContainer,
|
||||||
|
native.ResourceFormatRootContainers, native.ResourceFormatAllContainers,
|
||||||
|
}
|
||||||
|
|
||||||
|
func match(resource, pattern string) bool {
|
||||||
|
rTokens := strings.Split(resource, "/")
|
||||||
|
pToken := strings.Split(pattern, "/")
|
||||||
|
|
||||||
|
if len(rTokens) != len(pToken) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range rTokens {
|
||||||
|
if pToken[i] == "%s" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if pToken[i] != rTokens[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsNativeResourceNameValid(resource string) bool {
|
||||||
|
for _, pattern := range nativePatterns {
|
||||||
|
if match(resource, pattern) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
97
schema/native/util/validation_test.go
Normal file
97
schema/native/util/validation_test.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
expected bool
|
||||||
|
resource string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ResourceFormatNamespaceObjects",
|
||||||
|
expected: true,
|
||||||
|
resource: "native:object/RootNamespace/*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ResourceFormatNamespaceContainerObjects",
|
||||||
|
expected: true,
|
||||||
|
resource: "native:object/RootNamespace/BzQw5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB3R/*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ResourceFormatNamespaceContainerObject",
|
||||||
|
expected: true,
|
||||||
|
resource: "native:object/RootNamespace/BzQw5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB3R/AeZa5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB4E",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ResourceFormatRootObjects",
|
||||||
|
expected: true,
|
||||||
|
resource: "native:object//*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ResourceFormatRootContainerObjects",
|
||||||
|
expected: true,
|
||||||
|
resource: "native:object//BzQw5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB3R/*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ResourceFormatRootContainerObject",
|
||||||
|
expected: true,
|
||||||
|
resource: "native:object//BzQw5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB3R/AeZa5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB4E",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ResourceFormatAllObjects",
|
||||||
|
expected: true,
|
||||||
|
resource: "native:object/*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ResourceFormatNamespaceContainer",
|
||||||
|
expected: true,
|
||||||
|
resource: "native:container/RootNamespace/BzQw5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB3R",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ResourceFormatNamespaceContainers",
|
||||||
|
expected: true,
|
||||||
|
resource: "native:container/RootNamespace/*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ResourceFormatRootContainers",
|
||||||
|
expected: true,
|
||||||
|
resource: "native:container//*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ResourceFormatAllContainers",
|
||||||
|
expected: true,
|
||||||
|
resource: "native:container/*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid resource 1",
|
||||||
|
expected: false,
|
||||||
|
resource: "native:::container/*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid resource 2",
|
||||||
|
expected: false,
|
||||||
|
resource: "native:container/RootNamespace/w5HH3feoxFDD5tCTtoRzB3R/Bz726qzgLfxEE7wgtoRzB3R/RootNamespace",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsNativeResourceNameValid(t *testing.T) {
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
require.Equal(t, test.expected, IsNativeResourceNameValid(test.resource))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkIsNativeResourceNameValid(b *testing.B) {
|
||||||
|
for _, test := range tests {
|
||||||
|
b.Run(test.name, func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = IsNativeResourceNameValid(test.resource)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,4 +6,12 @@ const (
|
||||||
PropertyKeyDelimiter = "s3:delimiter"
|
PropertyKeyDelimiter = "s3:delimiter"
|
||||||
PropertyKeyPrefix = "s3:prefix"
|
PropertyKeyPrefix = "s3:prefix"
|
||||||
PropertyKeyVersionID = "s3:VersionId"
|
PropertyKeyVersionID = "s3:VersionId"
|
||||||
|
|
||||||
|
ResourceFormatS3All = "arn:aws:s3:::*"
|
||||||
|
ResourceFormatS3Bucket = "arn:aws:s3:::%s"
|
||||||
|
ResourceFormatS3BucketObjects = "arn:aws:s3:::%s/*"
|
||||||
|
ResourceFormatS3BucketObject = "arn:aws:s3:::%s/%s"
|
||||||
|
|
||||||
|
ResourceFormatIAMNamespaceUser = "arn:aws:iam::%s:user/%s"
|
||||||
|
ResourceFormatIAMNamespaceGroup = "arn:aws:iam::%s:group/%s"
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue