diff --git a/.forgejo/workflows/dco.yml b/.forgejo/workflows/dco.yml index 2374802..6746408 100644 --- a/.forgejo/workflows/dco.yml +++ b/.forgejo/workflows/dco.yml @@ -13,9 +13,9 @@ jobs: - name: Setup Go uses: actions/setup-go@v3 with: - go-version: '1.23' + go-version: '1.21' - name: Run commit format checker - uses: https://git.frostfs.info/TrueCloudLab/dco-go@v3 + uses: https://git.frostfs.info/TrueCloudLab/dco-go@v2 with: from: 'origin/${{ github.event.pull_request.base.ref }}' diff --git a/.forgejo/workflows/tests.yml b/.forgejo/workflows/tests.yml index 8dffdf9..f66a2c4 100644 --- a/.forgejo/workflows/tests.yml +++ b/.forgejo/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: '1.23' + go-version: '1.21' cache: true - name: Install linters @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go_versions: [ '1.22', '1.23' ] + go_versions: [ '1.20', '1.21' ] fail-fast: false steps: - uses: actions/checkout@v3 @@ -48,7 +48,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: '1.23' + go-version: '1.21' cache: true - name: Run tests @@ -63,7 +63,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: '1.23' + go-version: '1.21' cache: true - name: Install staticcheck diff --git a/.gitattributes b/.gitattributes index a582511..c7a3f7a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,2 @@ /**/*.pb.go -diff -merge /**/*.pb.go linguist-generated=true -/**/*_easyjson.go -diff -merge -/**/*_easyjson.go linguist-generated=true diff --git a/.golangci.yml b/.golangci.yml index 42b6b36..e10866d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -12,8 +12,7 @@ run: # output configuration options output: # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" - formats: - - format: tab + format: tab # all available settings of specific linters linters-settings: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e1fef9..aad8e01 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,6 +2,13 @@ ci: autofix_prs: false repos: + - repo: https://github.com/jorisroovers/gitlint + rev: v0.19.1 + hooks: + - id: gitlint + stages: [commit-msg] + - id: gitlint-ci + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: @@ -35,7 +42,7 @@ repos: hooks: - id: go-unit-tests name: go unit tests - entry: make test GOFLAGS='' + entry: make test pass_filenames: false types: [go] language: system diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index f164173..0000000 --- a/CODEOWNERS +++ /dev/null @@ -1,3 +0,0 @@ -.* @TrueCloudLab/storage-core-committers @TrueCloudLab/storage-core-developers @TrueCloudLab/storage-services-committers @TrueCloudLab/storage-services-developers -.forgejo/.* @potyarkin -Makefile @potyarkin diff --git a/Makefile b/Makefile index ca3b85d..debad31 100755 --- a/Makefile +++ b/Makefile @@ -1,12 +1,10 @@ #!/usr/bin/make -f -TRUECLOUDLAB_LINT_VERSION ?= 0.0.6 +TRUECLOUDLAB_LINT_VERSION ?= 0.0.2 TMP_DIR := .cache OUTPUT_LINT_DIR ?= $(shell pwd)/bin -LINT_VERSION ?= 1.60.1 +LINT_VERSION ?= 1.55.1 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 fmts: fmt imports @@ -22,10 +20,9 @@ imports: @goimports -w . # Run Unit Test with go test -test: GOFLAGS ?= "-count=1" test: @echo "⇒ Running go test" - @GOFLAGS="$(GOFLAGS)" go test ./... + @go test ./... -count=1 # Activate pre-commit hooks pre-commit: @@ -63,15 +60,3 @@ staticcheck-install: # Run staticcheck staticcheck-run: @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 diff --git a/docs/ape.md b/docs/ape.md deleted file mode 100644 index a239499..0000000 --- a/docs/ape.md +++ /dev/null @@ -1,85 +0,0 @@ -# Access policy engine - -## General overview - -### Purpose - -Access policy engine (APE) is aimed at checking if a request can be performed over a resource by looking up the set chains of rules. - -#### Terms - -| Term | Description | Structure overview | -| -------------- | -------------------------------------------------------------- | -------------- | -| `Request` | The action that is being performed on the `Resource`. | | -| `Resource` | The object that the request is being performed on. Check also [resource.md](./resource.md). | | -| `Chain` | A chain of `Rule`-s defined for a specific target. Chains are strictly distinguished by `Name`-s , i.e. chains with name `ingress` are not intersected with chains with name `s3`. Chains are stored in serialized format. | | -| `Rule` | `Rule` defines which status is returned if `Request` matches all conditions. | | -| `Name` | `Name` of a chain (do not confuse with chain ID). `Name` defines a layer of `Chain`'s usage, so chains are distinguished by `Name`-s. Basically, `Name` refers to a protocol. | String value (`ingress`, `s3`, `iam`). | -| `Target` | A scope of request. `Target` can be either simple (only namespace; only container; only user; only groups) or compound (namespace + container). | | -| `Engine` | `Engine` checks a request in a scope defined by `Target`. First, it is trying to match a request with rules defined in `LocalOverrideStorage` and, then, in `MorphRuleChainStorage`. | | - -#### Details - -Here some entities are overviewed in more detail. - -##### Resource - -`Resource`'s name is strictly formatted, the format is defined by a schema (`native`, `aws` etc.). Examples: - -```bash -# The resource is the particular object with the address within Root namespace -native:object//HRwWbb1bJjRms33kkA21hy4JdPfARaH3fW9NfuNN6Fgj/EbxzAdz5LB4uqxuz6crWKAumBNtZyK2rKsqQP7TdZvwr -# The resource is all objects within the container within Root namespace -native:object//HRwWbb1bJjRms33kkA21hy4JdPfARaH3fW9NfuNN6Fgj/* -# The resource is the particular container within the namespace -native:container/namespace1/HRwWbb1bJjRms33kkA21hy4JdPfARaH3fW9NfuNN6Fgj -# The resource is all containers within the namespace -native:container/namespace1/* -``` - -##### Rule - -`Rule` works out if: - - 1. a requests's operation matches the rule's `Actions`; - 2. resource name matches the rule's `Resources`; - 3. if all (or at least one if `Any=true`) conditions in `Condition` is met. Each condition defines how to retrieve -and compare the retrieved value. If `Condition`'s `Object` is set to `Resource` then the value is retrieved from the -resource's properties (example: container zone attribute). If `Object` is set to `Request`, the it's retrieved from the request's properties (example: actor's public key). - -###### Name matching - -`Resource`'s name in `Rule` may contain wildcard '*' that can be considered as a regular expression: - -```bash -# The resource is all objects within the container within Root namespace -native:object//HRwWbb1bJjRms33kkA21hy4JdPfARaH3fW9NfuNN6Fgj/* -``` - -If an incoming request has such a resource name, then names are matched: - -```bash -# The resource is all objects within the container within Root namespace -native:object//HRwWbb1bJjRms33kkA21hy4JdPfARaH3fW9NfuNN6Fgj/EbxzAdz5LB4uqxuz6crWKAumBNtZyK2rKsqQP7TdZvwr -``` - -If the incoming request has such a resource name that specifies a container's object within namespace, for instance, `namespicy`, -then matching does not work out: - -```bash -# The resource is all objects within the container within `namespicy` namespace: -native:object/namespicy/HRwWbb1bJjRms33kkA21hy4JdPfARaH3fW9NfuNN6Fgj/EbxzAdz5LB4uqxuz6crWKAumBNtZyK2rKsqQP7TdZvwr -``` - -##### Engine - -`Engine` is trying to match the request against **the target** looking up chain rules, firstly, in `LocalOverrideStorage` (these rules are also known as *local overrides*) and then in `MorphRuleChainStorage` (contract `Policy`). Both storages iterate chain rules according to the specified priority of the targets: `namespace` -> `container` -> `user` -> `groups`. - -#### Diagrams - -The diagram demonstrates a scenario in Storage node. The request `A` cannot be performed as APE matched -the request and returned `Access Denied` status. The request `B` is allowed and the client gets `OK` status. -![Storage node](images/ape/storage_node_ape.svg) - -The diagram demonstrates a complex scenario with S3, IAM and Storage node. -![S3 and IAM](images/ape/s3_ape.svg) diff --git a/docs/images/ape/s3_ape.puml b/docs/images/ape/s3_ape.puml deleted file mode 100644 index cf4e2ec..0000000 --- a/docs/images/ape/s3_ape.puml +++ /dev/null @@ -1,61 +0,0 @@ -@startuml s3 ape - -participant "Client" as client - -participant "IAM" as iam -participant "IAM -> APE converter" as converter - -box "S3" #HotPink - participant "S3 gateway" as s3 -end box - -box "Access Policy Engine (as s3 middleware)" #LightPink - participant "Local override storage" as s3localOverrides - participant "Chain router" as s3chainRouter -end box - -box "Policy contract (shared)" - participant "Morph rule storage" as morphRuleStorage -end box - -box "Access Policy Engine (as storage middleware)" #LightGreen - participant "Chain Router" as storageChainRouter - participant "Local override storage" as storageLocalOverrides -end box - -box "Storage node" #Green - participant "Object service" as obj - participant "Control service" as control -end box - -group Request IAM to set a policy - client -> iam : Set IAM policy - iam -> converter : Convert IAM policy - converter -> iam : Return APE chain - iam -> morphRuleStorage : Store IAM policy and APE chain - iam -> s3localOverrides : Set S3 local overrides - iam -> client : OK -end - -group Request S3 to set a policy - client -> s3 : Set bucket policy - s3 -> converter : Convert IAM policy - converter -> s3 : Return APE chain - s3 -> morphRuleStorage : Store bucket policy and APE chain - s3 -> client : OK -end - -group Get object - client -> s3: GetObject - s3 -> s3chainRouter: Check if APE allows request for S3 - note over s3chainRouter: matching the request with overrides and rules - s3chainRouter -> s3: Status: ALLOW - s3 -> obj: Get object - obj -> storageChainRouter: Check if APE allows the request - note over storageChainRouter : matching the request with overrides and rules - storageChainRouter -> obj: Status: ALLOW - obj -> s3: Response: OK, Object - s3 -> client: Response: OK, Object -end - -@enduml \ No newline at end of file diff --git a/docs/images/ape/s3_ape.svg b/docs/images/ape/s3_ape.svg deleted file mode 100644 index bd119c0..0000000 --- a/docs/images/ape/s3_ape.svg +++ /dev/null @@ -1,73 +0,0 @@ -S3Access Policy Engine (as s3 middleware)Policy contract (shared)Access Policy Engine (as storage middleware)Storage nodeClientClientIAMIAMIAM -> APE converterIAM -> APE converterS3 gatewayS3 gatewayLocal override storageLocal override storageChain routerChain routerMorph rule storageMorph rule storageChain RouterChain RouterLocal override storageLocal override storageObject serviceObject serviceControl serviceControl serviceRequest IAM to set a policySet IAM policyConvert IAM policyReturn APE chainStore IAM policy and APE chainSet S3 local overridesOKRequest S3 to set a policySet bucket policyConvert IAM policyReturn APE chainStore bucket policy and APE chainOKGet objectGetObjectCheck if APE allows request for S3matching the request with overrides and rulesStatus: ALLOWGet objectCheck if APE allows the requestmatching the request with overrides and rulesStatus: ALLOWResponse: OK, ObjectResponse: OK, Object \ No newline at end of file diff --git a/docs/images/ape/storage_node_ape.puml b/docs/images/ape/storage_node_ape.puml deleted file mode 100644 index ddf55e3..0000000 --- a/docs/images/ape/storage_node_ape.puml +++ /dev/null @@ -1,46 +0,0 @@ -@startuml storage node ape -!pragma teoz true - -participant "Administrator" as administrator -participant "Client" as client - -box "Storage node" #Green - participant "Object service" as obj - participant "Control service" as control -end box - -box "Access Policy Engine" #LightGreen - participant "Local override storage" as localOverrides - participant "Chain Router" as chainRouter - participant "Morph rule storage" as morphRuleStorage -end box - -group Set local override - client -> control: Add local override - control -> localOverrides: Save override in DB - localOverrides -> control: OK - control -> client: OK -end - -group Update state in Policy contract - administrator -> morphRuleStorage: Add chain - morphRuleStorage -> administrator: OK -end - -group Perform a request A - client -> obj : Sending a request - obj -> chainRouter: Check if APE allows the request - note over chainRouter : Fetches local overrides and rules defined for a target/targets and looks for a match - chainRouter -> obj: APE returns status: "ACCESS DENIED" - obj -> client: Response: "the request is denied" -end - -group Perform a request B - client -> obj : Sending a request - obj -> chainRouter: Check if APE allows the request - note over chainRouter : Fetches local overrides and rules defined for a target/targets and looks for a match - chainRouter -> obj: APE returns status: "ALLOW" - obj -> client: Response: "OK" -end - -@enduml \ No newline at end of file diff --git a/docs/images/ape/storage_node_ape.svg b/docs/images/ape/storage_node_ape.svg deleted file mode 100644 index 6e84081..0000000 --- a/docs/images/ape/storage_node_ape.svg +++ /dev/null @@ -1,58 +0,0 @@ -Storage nodeAccess Policy EngineAdministratorClientObject serviceControl serviceLocal override storageChain RouterMorph rule storageAdministratorClientObject serviceControl serviceLocal override storageChain RouterMorph rule storageSet local overrideAdd local overrideSave override in DBOKOKUpdate state in Policy contractAdd chainOKPerform a request ASending a requestCheck if APE allows the requestFetches local overrides and rules defined for a target/targets and looks for a matchAPE returns status: "ACCESS DENIED"Response: "the request is denied"Perform a request BSending a requestCheck if APE allows the requestFetches local overrides and rules defined for a target/targets and looks for a matchAPE returns status: "ALLOW"Response: "OK" \ No newline at end of file diff --git a/docs/policy_converters.md b/docs/policy_converters.md deleted file mode 100644 index 5a2b1ce..0000000 --- a/docs/policy_converters.md +++ /dev/null @@ -1,439 +0,0 @@ -# Policy converters - -This repository contains converters that provide opportunities to -transform [AWS IAM policies](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies.html) to inner -FrostFS policy format. This document describes such transformations. - -## FrostFS - -As it was mentioned there are converters that transform AWS IAM policies to FrostFS. -Here common examples of AWS: - -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "s3:*" - ], - "Resource": "*" - } - ] -} -``` - -and FrostFS: - -```json -{ - "ID": "c29tZS1pZA==", - "Rules": [ - { - "Status": "Allow", - "Actions": { - "Inverted": false, - "Names": [ - "s3:*" - ] - }, - "Resources": { - "Inverted": false, - "Names": [ - "*" - ] - }, - "Any": false, - "Condition": null - } - ], - "MatchType": "DenyPriority" -} -``` - -policies. - -Despite there is only one FrostFS format, we have two converters (`s3` and `native`). The reason is S3 gateway and -Storage node have different actions and resource naming: - -* S3 has [a lot of methods](https://docs.aws.amazon.com/AmazonS3/latest/API/API_Operations.html) and operates with - bucket/object -* Storage node has only 6 container and 7 object methods and operates container/object (that has different format) - -The following sections describe each transformation more precisely ([common](#common) sections contains shared concepts) - -### Common - -#### Fields - -Rough json main fields mapping: - -| AWS policy field | FrostFS policy field | Comment | -|------------------|----------------------|------------------------------------------------------------------| -| `Version` | - | Not applicable | -| `Statement` | `Rules` | | -| `Effect` | `Status` | | -| `Action` | `Actions.Names` | `Actions.Inverted` = false | -| `NotAction` | `Actions.Names` | `Actions.Inverted` = true | -| `Resource` | `Resources.Names` | `Resources.Inverted` = false | -| `NotResource` | `Resources.Names` | `Resources.Inverted` = true | -| `Condition` | `Condition` | `Any` = false, that means the conditions must be hold altogether | -| `Principal` | - | Expressed via conditions (depends on s3/native converters) | - -### Conditions - -Each condition in FrostFS policy can add requirements to some request/resource properties -and consists of the following fields: - -| Field | Description | -|----------|-------------------------------------------------------------------------------------------| -| `Op` | Condition type operation (`StringEqual`, `NumericEqual` etc) | -| `Object` | Property type to which condition can be applied (`Request` property, `Resource` property) | -| `Key` | Property key | -| `Value` | Property value | - -Conditions operators: - -| AWS conditions operator | FrostFS condition operator | Comment | -|-----------------------------|-----------------------------|-------------------------------------------------------------------| -| `StringEquals` | `StringEquals` | | -| `StringNotEquals` | `StringNotEquals` | | -| `StringEqualsIgnoreCase` | `StringEqualsIgnoreCase` | | -| `StringNotEqualsIgnoreCase` | `StringNotEqualsIgnoreCase` | | -| `StringLike` | `StringLike` | | -| `StringNotLike` | `StringNotLike` | | -| `NumericEquals` | `NumericEquals` | | -| `NumericNotEquals` | `NumericNotEquals` | | -| `NumericLessThan` | `NumericLessThan` | | -| `NumericLessThanEquals` | `NumericLessThanEquals` | | -| `NumericGreaterThan` | `NumericGreaterThan` | | -| `NumericGreaterThanEquals` | `NumericGreaterThanEquals` | | -| `DateEquals` | `StringEquals` | Date transforms to unix timestamp to be compared as string | -| `DateNotEquals` | `StringNotEquals` | Date transforms to unix timestamp to be compared as string | -| `DateLessThan` | `StringEqualsIgnoreCase` | Date transforms to unix timestamp to be compared as string | -| `DateLessThanEquals` | `StringNotEqualsIgnoreCase` | Date transforms to unix timestamp to be compared as string | -| `DateGreaterThan` | `StringLike` | Date transforms to unix timestamp to be compared as string | -| `DateGreaterThanEquals` | `StringNotLike` | Date transforms to unix timestamp to be compared as string | -| `Bool` | `StringEqualsIgnoreCase` | | -| `IpAddress` | `IPAddress` | | -| `NotIpAddress` | `NotIPAddress` | | -| `ArnEquals` | `StringEquals` | | -| `ArnLike` | `StringLike` | | -| `ArnNotEquals` | `StringNotEquals` | | -| `ArnNotLike` | `StringNotLike` | | -| `SliceContains` | `SliceContains` | AWS spec doesn't contain such operator. This is FrostFS extension | - -For example, AWS conditions: - -```json -{ - "Condition": { - "ArnEquals": {"key16": ["val16"]}, - "ArnNotEquals": {"key18": ["val18"]}, - "ArnNotLike": {"key19": ["val19"]}, - "Bool": {"key13": ["True"]}, - "DateEquals": {"key7": ["2006-01-02T15:04:05+07:00"]}, - "DateGreaterThan": {"key11": ["2006-01-02T15:04:05-01:00"]}, - "DateGreaterThanEquals": {"key12": ["2006-01-02T15:04:05-03:00"]}, - "DateLessThan": {"key9": ["2006-01-02T15:04:05+06:00"]}, - "DateLessThanEquals": {"key10": ["2006-01-02T15:04:05+03:00"]}, - "DateNotEquals": {"key8": ["2006-01-02T15:04:05Z"]}, - "NumericEquals": {"key20": ["-20"]}, - "NumericGreaterThan": {"key24": ["-24.24"]}, - "NumericGreaterThanEquals": {"key25": ["+25.25"]}, - "NumericLessThan": {"key22": ["0"]}, - "NumericLessThanEquals": {"key23": ["23.23"]}, - "NumericNotEquals": {"key21": ["+21"]}, - "StringEquals": {"key1": ["val0"]}, - "StringEqualsIgnoreCase": {"key3": ["val3"]}, - "StringLike": {"key5": ["val5"]}, - "StringNotEquals": {"key2": ["val2"]}, - "StringNotEqualsIgnoreCase": {"key4": ["val4"]}, - "StringNotLike": {"key6": ["val6"]} - } -} -``` - -transforms to FrostFS conditions: - -```json -{ - "Condition": [ - {"Op": "StringLike", "Object": "Request", "Key": "key5", "Value": "val5"}, - {"Op": "StringNotEquals", "Object": "Request", "Key": "key2", "Value": "val2"}, - {"Op": "StringGreaterThan", "Object": "Request", "Key": "key11", "Value": "1136217845"}, - {"Op": "StringGreaterThanEquals", "Object": "Request", "Key": "key12", "Value": "1136225045"}, - {"Op": "StringLessThan", "Object": "Request", "Key": "key9", "Value": "1136192645"}, - {"Op": "StringEqualsIgnoreCase", "Object": "Request", "Key": "key3", "Value": "val3"}, - {"Op": "StringEquals", "Object": "Request", "Key": "key16", "Value": "val16"}, - {"Op": "NumericLessThanEquals", "Object": "Request", "Key": "key23", "Value": "23.23"}, - {"Op": "StringNotEqualsIgnoreCase", "Object": "Request", "Key": "key4", "Value": "val4"}, - {"Op": "StringEquals", "Object": "Request", "Key": "key1", "Value": "val0"}, - {"Op": "StringLessThanEquals", "Object": "Request", "Key": "key10", "Value": "1136203445"}, - {"Op": "NumericGreaterThan", "Object": "Request", "Key": "key24", "Value": "-24.24"}, - {"Op": "NumericGreaterThanEquals", "Object": "Request", "Key": "key25", "Value": "+25.25"}, - {"Op": "NumericLessThan", "Object": "Request", "Key": "key22", "Value": "0"}, - {"Op": "StringNotEquals", "Object": "Request", "Key": "key8", "Value": "1136214245"}, - {"Op": "NumericEquals", "Object": "Request", "Key": "key20", "Value": "-20"}, - {"Op": "NumericNotEquals", "Object": "Request", "Key": "key21", "Value": "+21"}, - {"Op": "StringNotLike", "Object": "Request", "Key": "key6", "Value": "val6"}, - {"Op": "StringNotEquals", "Object": "Request", "Key": "key18", "Value": "val18"}, - {"Op": "StringNotLike", "Object": "Request", "Key": "key19", "Value": "val19"}, - {"Op": "StringEqualsIgnoreCase", "Object": "Request", "Key": "key13", "Value": "True"}, - {"Op": "StringEquals", "Object": "Request", "Key": "key7", "Value": "1136189045"} - ] -} -``` - -### S3 - -#### Actions - -Each action allows some s3-gw methods, so we must transform action to specific method names -(you can see exact mapping in table in [this file](../iam/converter_s3.go)). - -For example the following actions: - -```json -{ - "Action": [ - "s3:DeleteObject", - "iam:CreateUser" - ] -} -``` - -transforms to - -```json -{ - "Actions": { - "Inverted": false, - "Names": [ - "s3:DeleteObject", - "s3:DeleteMultipleObjects", - "iam:CreateUser" - ] - } -} -``` - -As we can see any `iam:*` action transformed as it is. But `s3:*` actions transforms according to -[spec rules](https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazons3.html) and s3-gw -[method names](https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/src/commit/2ab655b909c40db6f7a4e41e07d8b99167f791bd/api/middleware/constants.go#L3-L76). - -#### Resources - -Resource is transformed as it is: - -```json -{ - "Resource": [ - "arn:aws:s3:::bucket/object" - ] -} -``` - -```json -{ - "Resources": { - "Inverted": false, - "Names": [ - "arn:aws:s3:::bucket/object" - ] - } -} -``` - -#### Principals - -To check user s3-gw uses special condition request property (`Owner`), so when AWS policy contains principal field -it transforms to rule with appropriate condition. To get correct `Owner` property value special user resolver -(`S3Resolver` interface in [converter_s3 file](../iam/converter_s3.go)) must be provided into convert function. - -For example such AWS json statement: - -```json -{ - "Effect": "Allow", - "Action": "*", - "Resource": "*", - "Principal": { - "AWS": "arn:aws:iam::111122223333:user/JohnDoe" - } -} -``` - -transforms to the following FrostFS rule: - -```json -{ - "Status": "Allow", - "Actions": { - "Inverted": false, - "Names": [ - "*" - ] - }, - "Resources": { - "Inverted": false, - "Names": [ - "*" - ] - }, - "Any": false, - "Condition": [ - { - "Op": "StringEquals", - "Object": "Request", - "Key": "Owner", - "Value": "NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM" - } - ] -} -``` - -### Native - -#### Actions - -Each action allows some frostfs methods, so we must transform action to specific method names -(you can see exact mapping in table in [this file](../iam/converter_native.go)). - -For example the following actions: - -```json -{ - "Action": [ - "s3:DeleteObject", - "iam:CreateUser" - ] -} -``` - -transforms to - -```json -{ - "Actions": { - "Inverted": false, - "Names": [ - "PutObject", - "HeadObject", - "GetObject", - "RangeObject", - "GetContainer", - "DeleteObject" - ] - } -} -``` - -> **Note:** Only subset of s3:* actions can be transformed (exact value you can see in mapping table mentioned before). -> If all provided actions is not applicable converter function returns appropriate error. - -Native methods (to which original actions are transformed) depend on which methods are invoked by appropriate s3-gw -[method](https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/src/commit/2ab655b909c40db6f7a4e41e07d8b99167f791bd/api/middleware/constants.go#L3-L76). - -So in example above s3-gw during performing DeleteObject methods invokes the following methods: -`["PutObject","HeadObject","GetObject","RangeObject","GetContainer","DeleteObject"]` - -#### Resources - -To transform resources the following is being performed: - -* Bucket name is resoled to container id (by providing `NativeResolver` interface implementation to converter) -* Object name is transformed to condition with special `FilePath` attribute - (that present on every object that was uploaded via s3-gw) - -For example, the following AWS policy statement: - -```json -{ - "Principal": "*", - "Effect": "Allow", - "Action": "*", - "Resource": "arn:aws:s3:::bucket/object" -} -``` - -transforms to FrostFS native policy rule: - -```json -{ - "Status": "Allow", - "Actions": { - "Inverted": false, - "Names": [ - "*" - ] - }, - "Resources": { - "Inverted": false, - "Names": [ - "native:object//bucket/HFq67qbfhFEiEL7qDXqayo3F78yAvxXSXzwSa2hKM9bH/*", - "native:container//bucket/HFq67qbfhFEiEL7qDXqayo3F78yAvxXSXzwSa2hKM9bH" - ] - }, - "Any": false, - "Condition": [ - { - "Op": "StringLike", - "Object": "Resource", - "Key": "FilePath", - "Value": "object" - } - ] -} -``` - -#### Principals - -To check user s3-gw uses special condition request property (`$Actor:publicKey`), so when AWS policy contains principal -field it transforms to rule with appropriate condition. To get correct `$Actor:publicKey` property value -special user resolver (`NativeResolver` interface in [converter_native file](../iam/converter_native.go)) must be -provided into convert function. - -For example such AWS json statement: - -```json -{ - "Effect": "Allow", - "Action": "*", - "Resource": "*", - "Principal": { - "AWS": "arn:aws:iam::111122223333:user/JohnDoe" - } -} -``` - -transforms to the following FrostFS rule: - -```json -{ - "Status": "Allow", - "Actions": { - "Inverted": false, - "Names": [ - "*" - ] - }, - "Resources": { - "Inverted": false, - "Names": [ - "native:object/*", - "native:container/*" - ] - }, - "Any": false, - "Condition": [ - { - "Op": "StringEquals", - "Object": "Request", - "Key": "$Actor:publicKey", - "Value": "031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a" - } - ] -} -``` diff --git a/docs/resource.md b/docs/resource.md deleted file mode 100644 index 0d49cbc..0000000 --- a/docs/resource.md +++ /dev/null @@ -1,20 +0,0 @@ -# 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. diff --git a/go.mod b/go.mod index 4fa2847..28836a0 100644 --- a/go.mod +++ b/go.mod @@ -1,35 +1,25 @@ module git.frostfs.info/TrueCloudLab/policy-engine -go 1.22 +go 1.20 require ( - git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409111539-e7a05a49ff45 - github.com/google/uuid v1.3.1 - github.com/mailru/easyjson v0.7.7 - github.com/nspcc-dev/neo-go v0.105.0 + git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958 + github.com/nspcc-dev/neo-go v0.103.0 github.com/stretchr/testify v1.8.4 - golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect - github.com/golang/snappy v0.0.1 // indirect + github.com/google/uuid v1.3.0 // 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-20231123160306-3374ff1e7a3c // indirect - github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231127165613-b35f351f0ba0 // 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/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 // indirect - github.com/twmb/murmur3 v1.1.5 // indirect - go.etcd.io/bbolt v1.3.8 // indirect - go.uber.org/multierr v1.10.0 // indirect - go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.14.0 // indirect golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/text v0.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 855cef3..0453bd3 100644 --- a/go.sum +++ b/go.sum @@ -1,166 +1,43 @@ -git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409111539-e7a05a49ff45 h1:Tp4I+XOLp3VCJORfxSamQtj3RZNISbaLM4WD5iIzXxg= -git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409111539-e7a05a49ff45/go.mod h1:F/fe1OoIDKr5Bz99q4sriuHDuf3aZefZy9ZsCqEtgxc= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c= -github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= -github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= -github.com/consensys/gnark-crypto v0.12.2-0.20231013160410-1f65e75b6dfb h1:f0BMgIjhZy4lSRHCXFbQst85f5agZAjtDMixQqBWNpc= -github.com/consensys/gnark-crypto v0.12.2-0.20231013160410-1f65e75b6dfb/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958 h1:X9yPizADIhD3K/gdKVCthlAnf9aQ3UJJGnZgIwwixRQ= +git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958/go.mod h1:rQWdsG18NaiFvkJpMguJev913KD/yleHaniRBkUyt0o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM= -github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -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/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= -github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= 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/nspcc-dev/go-ordered-json v0.0.0-20231123160306-3374ff1e7a3c h1:OOQeE613BH93ICPq3eke5N78gWNeMjcBWkmD2NKyXVg= -github.com/nspcc-dev/go-ordered-json v0.0.0-20231123160306-3374ff1e7a3c/go.mod h1:79bEUDEviBHJMFV6Iq6in57FEOCMcRhfQnfaf0ETA5U= -github.com/nspcc-dev/neo-go v0.105.0 h1:vtNZYFEFySK8zRDhLzQYha849VzWrcKezlnq/oNQg/w= -github.com/nspcc-dev/neo-go v0.105.0/go.mod h1:6pchIHg5okeZO955RxpTh5q0sUI0vtpgPM6Q+no1rlI= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231127165613-b35f351f0ba0 h1:N+dMIBmteXjJpkH6UZ7HmNftuFxkqszfGLbhsEctnv0= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231127165613-b35f351f0ba0/go.mod h1:J/Mk6+nKeKSW4wygkZQFLQ6SkLOSGX5Ga0RuuuktEag= +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/go.mod h1:79bEUDEviBHJMFV6Iq6in57FEOCMcRhfQnfaf0ETA5U= +github.com/nspcc-dev/neo-go v0.103.0 h1:UVyWPhzZdfYFG35ORP3FRDLh8J/raRQ6m8SptDdlgfM= +github.com/nspcc-dev/neo-go v0.103.0/go.mod h1:x+wmcYqpZYJwLp1l/pHZrqNp3RSWlkMymWGDij3/OPo= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231020160724-c3955f87d1b5 h1:09CpI5uwsxb1EeFPIKQRwwWlfCmDD/Dwwh01lPiQScM= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231020160724-c3955f87d1b5/go.mod h1:J/Mk6+nKeKSW4wygkZQFLQ6SkLOSGX5Ga0RuuuktEag= github.com/nspcc-dev/rfc6979 v0.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE= github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= -github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs= -github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM= -github.com/twmb/murmur3 v1.1.5 h1:i9OLS9fkuLzBXjt6dptlAEyk58fJsSTXbRg3SgVyqgk= -github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= -github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= -github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c= -go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= -go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -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/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 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/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E= -golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= -rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/iam/converter.go b/iam/converter.go index 39d0819..b288de3 100644 --- a/iam/converter.go +++ b/iam/converter.go @@ -3,79 +3,15 @@ package iam import ( "errors" "fmt" - "net/netip" "strconv" "strings" "time" "unicode/utf8" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" - "git.frostfs.info/TrueCloudLab/policy-engine/schema/common" - "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" ) -const ( - s3ActionAbortMultipartUpload = "s3:AbortMultipartUpload" - s3ActionCreateBucket = "s3:CreateBucket" - s3ActionDeleteBucket = "s3:DeleteBucket" - s3ActionDeleteBucketPolicy = "s3:DeleteBucketPolicy" - s3ActionDeleteObject = "s3:DeleteObject" - s3ActionDeleteObjectTagging = "s3:DeleteObjectTagging" - s3ActionDeleteObjectVersion = "s3:DeleteObjectVersion" - s3ActionDeleteObjectVersionTagging = "s3:DeleteObjectVersionTagging" - s3ActionGetBucketACL = "s3:GetBucketAcl" - s3ActionGetBucketCORS = "s3:GetBucketCORS" - s3ActionGetBucketLocation = "s3:GetBucketLocation" - s3ActionGetBucketNotification = "s3:GetBucketNotification" - s3ActionGetBucketObjectLockConfiguration = "s3:GetBucketObjectLockConfiguration" - s3ActionGetBucketPolicy = "s3:GetBucketPolicy" - s3ActionGetBucketPolicyStatus = "s3:GetBucketPolicyStatus" - s3ActionGetBucketTagging = "s3:GetBucketTagging" - s3ActionGetBucketVersioning = "s3:GetBucketVersioning" - s3ActionGetLifecycleConfiguration = "s3:GetLifecycleConfiguration" - s3ActionGetObject = "s3:GetObject" - s3ActionGetObjectACL = "s3:GetObjectAcl" - s3ActionGetObjectAttributes = "s3:GetObjectAttributes" - s3ActionGetObjectLegalHold = "s3:GetObjectLegalHold" - s3ActionGetObjectRetention = "s3:GetObjectRetention" - s3ActionGetObjectTagging = "s3:GetObjectTagging" - s3ActionGetObjectVersion = "s3:GetObjectVersion" - s3ActionGetObjectVersionACL = "s3:GetObjectVersionAcl" - s3ActionGetObjectVersionAttributes = "s3:GetObjectVersionAttributes" - s3ActionGetObjectVersionTagging = "s3:GetObjectVersionTagging" - s3ActionListAllMyBuckets = "s3:ListAllMyBuckets" - s3ActionListBucket = "s3:ListBucket" - s3ActionListBucketMultipartUploads = "s3:ListBucketMultipartUploads" - s3ActionListBucketVersions = "s3:ListBucketVersions" - s3ActionListMultipartUploadParts = "s3:ListMultipartUploadParts" - s3ActionPutBucketACL = "s3:PutBucketAcl" - s3ActionPutBucketCORS = "s3:PutBucketCORS" - s3ActionPutBucketNotification = "s3:PutBucketNotification" - s3ActionPutBucketObjectLockConfiguration = "s3:PutBucketObjectLockConfiguration" - s3ActionPutBucketPolicy = "s3:PutBucketPolicy" - s3ActionPutBucketTagging = "s3:PutBucketTagging" - s3ActionPutBucketVersioning = "s3:PutBucketVersioning" - s3ActionPutLifecycleConfiguration = "s3:PutLifecycleConfiguration" - s3ActionPutObject = "s3:PutObject" - s3ActionPutObjectACL = "s3:PutObjectAcl" - s3ActionPutObjectLegalHold = "s3:PutObjectLegalHold" - s3ActionPutObjectRetention = "s3:PutObjectRetention" - s3ActionPutObjectTagging = "s3:PutObjectTagging" - s3ActionPutObjectVersionACL = "s3:PutObjectVersionAcl" - s3ActionPutObjectVersionTagging = "s3:PutObjectVersionTagging" - s3ActionPatchObject = "s3:PatchObject" - s3ActionPutBucketPublicAccessBlock = "s3:PutBucketPublicAccessBlock" - s3ActionGetBucketPublicAccessBlock = "s3:GetBucketPublicAccessBlock" -) - -const ( - condKeyAWSPrincipalARN = "aws:PrincipalArn" - condKeyAWSSourceIP = "aws:SourceIp" - condKeyAWSPrincipalTagPrefix = "aws:PrincipalTag/" - condKeyAWSRequestTagPrefix = "aws:RequestTag/" - condKeyAWSResourceTagPrefix = "aws:ResourceTag/" - userClaimTagPrefix = "tag-" -) +const condKeyAWSPrincipalARN = "aws:PrincipalArn" const ( // String condition operators. @@ -114,16 +50,12 @@ const ( CondArnLike string = "ArnLike" CondArnNotEquals string = "ArnNotEquals" CondArnNotLike string = "ArnNotLike" - - // Custom condition operators. - CondSliceContains string = "SliceContains" ) const ( arnIAMPrefix = "arn:aws:iam::" s3ResourcePrefix = "arn:aws:s3:::" s3ActionPrefix = "s3:" - iamActionPrefix = "iam:" ) var ( @@ -135,9 +67,6 @@ var ( // ErrInvalidActionFormat occurs when action has unknown/unsupported 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 @@ -186,10 +115,10 @@ func convertToChainCondition(c Conditions) ([]GroupedConditions, error) { } group.Conditions[i] = chain.Condition{ - Op: condType, - Kind: chain.KindRequest, - Key: transformKey(key), - Value: converted, + Op: condType, + Object: chain.ObjectRequest, + Key: key, + Value: converted, } } grouped = append(grouped, group) @@ -199,20 +128,6 @@ func convertToChainCondition(c Conditions) ([]GroupedConditions, error) { return grouped, nil } -func transformKey(key string) string { - tagName, isTag := strings.CutPrefix(key, condKeyAWSPrincipalTagPrefix) - if isTag { - return fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, userClaimTagPrefix+tagName) - } - - switch key { - case condKeyAWSSourceIP: - return common.PropertyKeyFrostFSSourceIP - } - - return key -} - func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFunction, error) { switch { case strings.HasPrefix(op, "String"): @@ -246,7 +161,8 @@ func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFuncti return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op) } case strings.HasPrefix(op, "Numeric"): - return numericConditionTypeAndConverter(op) + // TODO + return 0, nil, fmt.Errorf("currently nummeric conditions unsupported: '%s'", op) case strings.HasPrefix(op, "Date"): switch op { case CondDateEquals: @@ -267,30 +183,13 @@ func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFuncti case op == CondBool: return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil case op == CondIPAddress: - return chain.CondIPAddress, ipConvertFunction, nil + // todo consider using converters + // "203.0.113.0/24" -> "203.0.113.*", + // "2001:DB8:1234:5678::/64" -> "2001:DB8:1234:5678:*" + // or having specific condition type for IP + return chain.CondStringLike, noConvertFunction, nil case op == CondNotIPAddress: - return chain.CondNotIPAddress, ipConvertFunction, nil - case op == CondSliceContains: - return chain.CondSliceContains, noConvertFunction, nil - default: - return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op) - } -} - -func numericConditionTypeAndConverter(op string) (chain.ConditionType, convertFunction, error) { - switch op { - case CondNumericEquals: - return chain.CondNumericEquals, numericConvertFunction, nil - case CondNumericNotEquals: - return chain.CondNumericNotEquals, numericConvertFunction, nil - case CondNumericLessThan: - return chain.CondNumericLessThan, numericConvertFunction, nil - case CondNumericLessThanEquals: - return chain.CondNumericLessThanEquals, numericConvertFunction, nil - case CondNumericGreaterThan: - return chain.CondNumericGreaterThan, numericConvertFunction, nil - case CondNumericGreaterThanEquals: - return chain.CondNumericGreaterThanEquals, numericConvertFunction, nil + return chain.CondStringNotLike, noConvertFunction, nil default: return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op) } @@ -302,25 +201,6 @@ func noConvertFunction(val string) (string, error) { return val, nil } -func numericConvertFunction(val string) (string, error) { - if _, err := fixedn.Fixed8FromString(val); err == nil { - return val, nil - } - - return "", fmt.Errorf("invalid numeric value: '%s'", val) -} - -func ipConvertFunction(val string) (string, error) { - if _, err := netip.ParsePrefix(val); err != nil { - if _, err = netip.ParseAddr(val); err != nil { - return "", err - } - val += "/32" - } - - return val, nil -} - func dateConvertFunction(val string) (string, error) { if _, err := strconv.ParseInt(val, 10, 64); err == nil { return val, nil @@ -363,35 +243,53 @@ func parsePrincipalAsIAMUser(principal string) (account string, user string, err return account, user, nil } -func validateResource(resource string) error { +func parseResourceAsS3ARN(resource string) (bucket string, object string, err error) { if resource == Wildcard { - return nil + return Wildcard, Wildcard, nil } - if !strings.HasPrefix(resource, s3ResourcePrefix) && !strings.HasPrefix(resource, arnIAMPrefix) { - return ErrInvalidResourceFormat + if !strings.HasPrefix(resource, s3ResourcePrefix) { + return "", "", ErrInvalidResourceFormat } - index := strings.IndexByte(resource, Wildcard[0]) - if index != -1 && index != utf8.RuneCountInString(resource)-1 { - return ErrInvalidResourceFormat + // iam arn format arn:aws:s3:::/ + s3Resource := strings.TrimPrefix(resource, s3ResourcePrefix) + sepIndex := strings.Index(s3Resource, "/") + if sepIndex < 0 { + return s3Resource, Wildcard, nil } - return nil + bucket = s3Resource[:sepIndex] + object = s3Resource[sepIndex+1:] + if len(object) == 0 { + return bucket, Wildcard, nil + } + + if bucket == Wildcard && object != Wildcard { + return "", "", ErrInvalidResourceFormat + } + + return bucket, object, nil } -func validateAction(action string) (bool, error) { - isIAM := strings.HasPrefix(action, iamActionPrefix) - if !strings.HasPrefix(action, s3ActionPrefix) && !isIAM { - return false, ErrInvalidActionFormat +func parseActionAsS3Action(action string) (string, error) { + if action == Wildcard { + return Wildcard, nil } - index := strings.IndexByte(action, Wildcard[0]) - if index != -1 && index != utf8.RuneCountInString(action)-1 { - return false, ErrInvalidActionFormat + if !strings.HasPrefix(action, s3ActionPrefix) { + return "", ErrInvalidActionFormat } - return isIAM, nil + // iam arn format :s3: + s3Action := strings.TrimPrefix(action, s3ActionPrefix) + + index := strings.IndexByte(s3Action, Wildcard[0]) + if index != -1 && index != utf8.RuneCountInString(s3Action)-1 { + return "", ErrInvalidActionFormat + } + + return s3Action, nil } func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition { diff --git a/iam/converter_native.go b/iam/converter_native.go index 40373f4..4abfcdf 100644 --- a/iam/converter_native.go +++ b/iam/converter_native.go @@ -3,7 +3,6 @@ package iam import ( "errors" "fmt" - "strings" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" @@ -11,89 +10,28 @@ import ( const PropertyKeyFilePath = "FilePath" -var actionToNativeOpMap = map[string][]string{ - s3ActionAbortMultipartUpload: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, - s3ActionCreateBucket: {native.MethodGetContainer, native.MethodPutContainer, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodPutObject}, - s3ActionDeleteBucket: {native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject, native.MethodGetObject}, - s3ActionDeleteBucketPolicy: {native.MethodGetContainer}, - s3ActionDeleteObject: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject}, - s3ActionDeleteObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, - s3ActionDeleteObjectVersion: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject}, - s3ActionDeleteObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, - s3ActionGetBucketACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject}, - s3ActionGetBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, - s3ActionGetBucketLocation: {native.MethodGetContainer}, - s3ActionGetBucketNotification: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, - s3ActionGetBucketObjectLockConfiguration: {native.MethodGetContainer, native.MethodGetObject}, - s3ActionGetBucketPolicy: {native.MethodGetContainer}, - s3ActionGetBucketPolicyStatus: {native.MethodGetContainer}, - s3ActionGetBucketTagging: {native.MethodGetContainer, native.MethodGetObject}, - s3ActionGetBucketVersioning: {native.MethodGetContainer, native.MethodGetObject}, - s3ActionGetLifecycleConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, - s3ActionGetObject: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, - s3ActionGetObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject}, - s3ActionGetObjectAttributes: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, - s3ActionGetObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject}, - s3ActionGetObjectRetention: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject}, - s3ActionGetObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject}, - s3ActionGetObjectVersion: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, - s3ActionGetObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject}, - s3ActionGetObjectVersionAttributes: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject}, - s3ActionGetObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject}, - s3ActionListAllMyBuckets: {native.MethodListContainers, native.MethodGetContainer}, - s3ActionListBucket: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, - s3ActionListBucketMultipartUploads: {native.MethodGetContainer, native.MethodGetObject}, - s3ActionListBucketVersions: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, - s3ActionListMultipartUploadParts: {native.MethodGetContainer, native.MethodGetObject}, - s3ActionPutBucketACL: {native.MethodGetContainer, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodPutObject}, - s3ActionPutBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject}, - s3ActionPutBucketNotification: {native.MethodGetContainer, native.MethodHeadObject, native.MethodDeleteObject, native.MethodGetObject, native.MethodPutObject}, - s3ActionPutBucketObjectLockConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject}, - s3ActionPutBucketPolicy: {native.MethodGetContainer}, - s3ActionPutBucketTagging: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject}, - s3ActionPutBucketVersioning: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject}, - s3ActionPutLifecycleConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodPutObject, native.MethodDeleteObject}, - s3ActionPutObject: {native.MethodGetContainer, native.MethodPutObject, native.MethodGetObject, native.MethodHeadObject, native.MethodRangeObject}, - s3ActionPutObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject}, - s3ActionPutObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, - s3ActionPutObjectRetention: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, - s3ActionPutObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, - s3ActionPutObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject}, - s3ActionPutObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject}, - s3ActionPatchObject: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodPatchObject, native.MethodPutObject, native.MethodRangeObject}, - s3ActionPutBucketPublicAccessBlock: {native.MethodGetContainer, native.MethodPutObject, native.MethodDeleteObject, native.MethodGetObject}, - s3ActionGetBucketPublicAccessBlock: {native.MethodGetContainer, native.MethodGetObject}, +// ErrActionsNotApplicable occurs when failed to convert any actions. +var ErrActionsNotApplicable = errors.New("actions not applicable") + +var actionToOpMap = map[string][]string{ + supportedS3ActionDeleteObject: {native.MethodDeleteObject}, + supportedS3ActionGetObject: {native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, + supportedS3ActionHeadObject: {native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, + supportedS3ActionPutObject: {native.MethodPutObject}, + 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: {}, -} - -var errConditionKeyNotApplicable = errors.New("condition key is not applicable") +const ( + supportedS3ActionDeleteObject = "DeleteObject" + supportedS3ActionGetObject = "GetObject" + supportedS3ActionHeadObject = "HeadObject" + supportedS3ActionPutObject = "PutObject" + supportedS3ActionListBucket = "ListBucket" +) type NativeResolver interface { GetUserKey(account, name string) (string, error) - GetBucketInfo(bucket string) (*BucketInfo, error) -} - -type BucketInfo struct { - Namespace string - Container string + GetBucketCID(bucket string) (string, error) } func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, error) { @@ -105,11 +43,6 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro for _, statement := range p.Statement { status := formStatus(statement) - if status != chain.Allow { - // Most s3 methods share the same native operations. Deny rules must not affect shared native operations, - // therefore this code skips all deny rules for native protocol. Deny is applied for s3 protocol only, in this case. - continue - } action, actionInverted := statement.action() nativeActions, err := formNativeActionNames(action) @@ -122,16 +55,13 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro } resource, resourceInverted := statement.resource() - groupedResources, err := formNativeResourceNamesAndConditions(resource, resolver, getActionTypes(nativeActions)) + groupedResources, err := formNativeResourceNamesAndConditions(resource, resolver) if err != nil { return nil, err } groupedConditions, err := convertToNativeChainCondition(statement.Conditions, resolver) if err != nil { - if errors.Is(err, errConditionKeyNotApplicable) { - continue - } return nil, err } splitConditions := splitGroupedConditions(groupedConditions) @@ -144,12 +74,7 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro for _, groupedResource := range groupedResources { for _, principal := range principals { for _, conditions := range splitConditions { - var principalCondition []chain.Condition - if principal != Wildcard { - principalCondition = []chain.Condition{principalCondFn(principal)} - } - - ruleConditions := append(principalCondition, groupedResource.Conditions...) + ruleConditions := append([]chain.Condition{principalCondFn(principal)}, groupedResource.Conditions...) r := chain.Rule{ Status: status, @@ -173,23 +98,6 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro 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 = res.Object || isObj || action == Wildcard - res.Container = res.Container || isCnr || action == Wildcard - } - - return res -} - func getNativePrincipalsAndConditionFunc(statement Statement, resolver NativeResolver) ([]string, formPrincipalConditionFunc, error) { var principals []string var op chain.ConditionType @@ -217,43 +125,28 @@ func getNativePrincipalsAndConditionFunc(statement Statement, resolver NativeRes return principals, func(principal string) chain.Condition { return chain.Condition{ - Op: op, - Kind: chain.KindRequest, - Key: native.PropertyKeyActorPublicKey, - Value: principal, + Op: op, + Object: chain.ObjectRequest, + Key: native.PropertyKeyActorPublicKey, + Value: principal, } }, nil } func convertToNativeChainCondition(c Conditions, resolver NativeResolver) ([]GroupedConditions, error) { return convertToChainConditions(c, func(gr GroupedConditions) (GroupedConditions, error) { - res := GroupedConditions{ - Conditions: make([]chain.Condition, 0, len(gr.Conditions)), - Any: gr.Any, - } - for i := range gr.Conditions { - switch { - case gr.Conditions[i].Key == condKeyAWSMFAPresent: - return GroupedConditions{}, errConditionKeyNotApplicable - case gr.Conditions[i].Key == condKeyAWSPrincipalARN: + if gr.Conditions[i].Key == condKeyAWSPrincipalARN { gr.Conditions[i].Key = native.PropertyKeyActorPublicKey val, err := formPrincipalKey(gr.Conditions[i].Value, resolver) if err != nil { return GroupedConditions{}, err } gr.Conditions[i].Value = val - res.Conditions = append(res.Conditions, gr.Conditions[i]) - case strings.HasPrefix(gr.Conditions[i].Key, condKeyAWSRequestTagPrefix) || - strings.HasPrefix(gr.Conditions[i].Key, condKeyAWSResourceTagPrefix): - // Tags exist only in S3 requests, so native protocol should not process such conditions. - continue - default: - res.Conditions = append(res.Conditions, gr.Conditions[i]) } } - return res, nil + return gr, nil }) } @@ -262,106 +155,53 @@ type GroupedResources struct { Conditions []chain.Condition } -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 - } - +func formNativeResourceNamesAndConditions(names []string, resolver NativeResolver) ([]GroupedResources, error) { res := make([]GroupedResources, 0, len(names)) - combined := make(map[string]struct{}) + var combined []string - for _, resource := range names { - if err := validateResource(resource); err != nil { - return nil, err - } - - if resource == Wildcard { - res = res[:0] - return append(res, formWildcardNativeResource(actionTypes)), nil - } - - 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) + for i := range names { + bkt, obj, err := parseResourceAsS3ARN(names[i]) if err != nil { return nil, err } - if obj == Wildcard && actionTypes.Object { // this corresponds to arn:aws:s3:::BUCKET/ or arn:aws:s3:::BUCKET/* - combined[fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)] = struct{}{} - combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{} - continue + if bkt == Wildcard { + res = res[:0] + return append(res, GroupedResources{Names: []string{native.ResourceFormatAllObjects}}), nil } - if obj == "" && actionTypes.Container { // this corresponds to arn:aws:s3:::BUCKET - combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{} + + cnrID, err := resolver.GetBucketCID(bkt) + if err != nil { + return nil, err + } + resource := fmt.Sprintf(native.ResourceFormatRootContainerObjects, cnrID) + + if obj == Wildcard { + combined = append(combined, resource) continue } res = append(res, GroupedResources{ - Names: []string{ - fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container), - fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container), - }, + Names: []string{resource}, Conditions: []chain.Condition{ { - Op: chain.CondStringLike, - Kind: chain.KindResource, - Key: PropertyKeyFilePath, - Value: obj, + Op: chain.CondStringLike, + Object: chain.ObjectResource, + Key: PropertyKeyFilePath, + Value: obj, }, }, }) } if len(combined) != 0 { - gr := GroupedResources{Names: make([]string, 0, len(combined))} - for key := range combined { - gr.Names = append(gr.Names, key) - } - - res = append(res, gr) + res = append(res, GroupedResources{Names: combined}) } 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) { res := make([]string, len(principal)) @@ -390,39 +230,17 @@ func formPrincipalKey(principal string, resolver NativeResolver) (string, error) } func formNativeActionNames(names []string) ([]string, error) { - uniqueActions := make(map[string]struct{}, len(names)) + res := make([]string, 0, len(names)) - for _, action := range names { - if action == Wildcard { - return []string{Wildcard}, nil - } - - isIAM, err := validateAction(action) + for i := range names { + action, err := parseActionAsS3Action(names[i]) if err != nil { return nil, err } - - if isIAM { - continue - } - - if action[len(s3ActionPrefix):] == Wildcard { + if action == Wildcard { return []string{Wildcard}, nil } - - nativeActions := actionToNativeOpMap[action] - if len(nativeActions) == 0 { - return nil, ErrActionsNotApplicable - } - - for _, nativeAction := range nativeActions { - uniqueActions[nativeAction] = struct{}{} - } - } - - res := make([]string, 0, len(uniqueActions)) - for key := range uniqueActions { - res = append(res, key) + res = append(res, actionToOpMap[action]...) } return res, nil diff --git a/iam/converter_s3.go b/iam/converter_s3.go index 7aa3190..094c3d3 100644 --- a/iam/converter_s3.go +++ b/iam/converter_s3.go @@ -2,73 +2,11 @@ package iam import ( "fmt" - "strings" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/schema/s3" ) -const condKeyAWSMFAPresent = "aws:MultiFactorAuthPresent" - -var actionToS3OpMap = map[string][]string{ - s3ActionAbortMultipartUpload: {s3ActionAbortMultipartUpload}, - s3ActionCreateBucket: {s3ActionCreateBucket}, - s3ActionDeleteBucket: {s3ActionDeleteBucket}, - s3ActionDeleteBucketPolicy: {s3ActionDeleteBucketPolicy}, - s3ActionDeleteObjectTagging: {s3ActionDeleteObjectTagging}, - s3ActionGetBucketLocation: {s3ActionGetBucketLocation}, - s3ActionGetBucketNotification: {s3ActionGetBucketNotification}, - s3ActionGetBucketPolicy: {s3ActionGetBucketPolicy}, - s3ActionGetBucketPolicyStatus: {s3ActionGetBucketPolicyStatus}, - s3ActionGetBucketTagging: {s3ActionGetBucketTagging}, - s3ActionGetBucketVersioning: {s3ActionGetBucketVersioning}, - s3ActionGetObjectAttributes: {s3ActionGetObjectAttributes}, - s3ActionGetObjectLegalHold: {s3ActionGetObjectLegalHold}, - s3ActionGetObjectRetention: {s3ActionGetObjectRetention}, - s3ActionGetObjectTagging: {s3ActionGetObjectTagging}, - s3ActionPutBucketNotification: {s3ActionPutBucketNotification}, - s3ActionPutBucketPolicy: {s3ActionPutBucketPolicy}, - s3ActionPutBucketVersioning: {s3ActionPutBucketVersioning}, - s3ActionPutObjectLegalHold: {s3ActionPutObjectLegalHold}, - s3ActionPutObjectRetention: {s3ActionPutObjectRetention}, - s3ActionPutObjectTagging: {s3ActionPutObjectTagging}, - s3ActionPatchObject: {s3ActionPatchObject}, - - s3ActionListAllMyBuckets: {"s3:ListBuckets"}, - s3ActionListBucket: {"s3:HeadBucket", "s3:GetBucketLocation", "s3:ListObjectsV1", "s3:ListObjectsV2"}, - s3ActionListBucketVersions: {"s3:ListBucketObjectVersions"}, - s3ActionListBucketMultipartUploads: {"s3:ListMultipartUploads"}, - s3ActionGetBucketObjectLockConfiguration: {"s3:GetBucketObjectLockConfig"}, - s3ActionGetLifecycleConfiguration: {"s3:GetBucketLifecycle"}, - s3ActionGetBucketACL: {"s3:GetBucketACL"}, - s3ActionGetBucketCORS: {"s3:GetBucketCors"}, - s3ActionPutBucketTagging: {"s3:PutBucketTagging", "s3:DeleteBucketTagging"}, - s3ActionPutBucketObjectLockConfiguration: {"s3:PutBucketObjectLockConfig"}, - s3ActionPutLifecycleConfiguration: {"s3:PutBucketLifecycle", "s3:DeleteBucketLifecycle"}, - s3ActionPutBucketACL: {"s3:PutBucketACL"}, - s3ActionPutBucketCORS: {"s3:PutBucketCors", "s3:DeleteBucketCors"}, - - s3ActionListMultipartUploadParts: {"s3:ListParts"}, - s3ActionGetObjectACL: {"s3:GetObjectACL"}, - s3ActionGetObject: {"s3:GetObject", "s3:HeadObject"}, - s3ActionGetObjectVersion: {"s3:GetObject", "s3:HeadObject"}, - s3ActionGetObjectVersionACL: {"s3:GetObjectACL"}, - s3ActionGetObjectVersionAttributes: {"s3:GetObjectAttributes"}, - s3ActionGetObjectVersionTagging: {"s3:GetObjectTagging"}, - s3ActionPutObjectACL: {"s3:PutObjectACL"}, - s3ActionPutObjectVersionACL: {"s3:PutObjectACL"}, - s3ActionPutObjectVersionTagging: {"s3:PutObjectTagging"}, - s3ActionPutObject: { - "s3:PutObject", "s3:PostObject", "s3:CopyObject", - "s3:UploadPart", "s3:UploadPartCopy", "s3:CreateMultipartUpload", "s3:CompleteMultipartUpload", - }, - s3ActionDeleteObjectVersionTagging: {"s3:DeleteObjectTagging"}, - s3ActionDeleteObject: {"s3:DeleteObject", "s3:DeleteMultipleObjects"}, - s3ActionDeleteObjectVersion: {"s3:DeleteObject", "s3:DeleteMultipleObjects"}, - s3ActionPutBucketPublicAccessBlock: {"s3:PutPublicAccessBlock", "s3:DeletePublicAccessBlock"}, - s3ActionGetBucketPublicAccessBlock: {"s3:GetPublicAccessBlock"}, -} - type S3Resolver interface { GetUserAddress(account, user string) (string, error) } @@ -83,21 +21,19 @@ func ConvertToS3Chain(p Policy, resolver S3Resolver) (*chain.Chain, error) { for _, statement := range p.Statement { status := formStatus(statement) - actions, actionInverted := statement.action() - s3Actions, err := formS3ActionNames(actions) + action, actionInverted := statement.action() + s3Actions, err := formS3ActionNames(action) if err != nil { return nil, err } ruleAction := chain.Actions{Inverted: actionInverted, Names: s3Actions} - if len(ruleAction.Names) == 0 { - continue - } - resources, resourceInverted := statement.resource() - if err := validateS3ResourceNames(resources); err != nil { + resource, resourceInverted := statement.resource() + s3Resources, err := formS3ResourceNames(resource) + if err != nil { return nil, err } - ruleResource := chain.Resources{Inverted: resourceInverted, Names: resources} + ruleResource := chain.Resources{Inverted: resourceInverted, Names: s3Resources} groupedConditions, err := convertToS3ChainCondition(statement.Conditions, resolver) if err != nil { @@ -112,26 +48,17 @@ func ConvertToS3Chain(p Policy, resolver S3Resolver) (*chain.Chain, error) { for _, principal := range principals { for _, conditions := range splitConditions { - var principalCondition []chain.Condition - if principal != Wildcard { - principalCondition = []chain.Condition{principalCondFn(principal)} - } - r := chain.Rule{ Status: status, Actions: ruleAction, Resources: ruleResource, - Condition: append(principalCondition, conditions...), + Condition: append([]chain.Condition{principalCondFn(principal)}, conditions...), } engineChain.Rules = append(engineChain.Rules, r) } } } - if len(engineChain.Rules) == 0 { - return nil, ErrActionsNotApplicable - } - return &engineChain, nil } @@ -162,10 +89,10 @@ func getS3PrincipalsAndConditionFunc(statement Statement, resolver S3Resolver) ( return principals, func(principal string) chain.Condition { return chain.Condition{ - Op: op, - Kind: chain.KindRequest, - Key: s3.PropertyKeyOwner, - Value: principal, + Op: op, + Object: chain.ObjectRequest, + Key: s3.PropertyKeyOwner, + Value: principal, } }, nil } @@ -173,19 +100,13 @@ func getS3PrincipalsAndConditionFunc(statement Statement, resolver S3Resolver) ( func convertToS3ChainCondition(c Conditions, resolver S3Resolver) ([]GroupedConditions, error) { return convertToChainConditions(c, func(gr GroupedConditions) (GroupedConditions, error) { for i := range gr.Conditions { - switch { - case gr.Conditions[i].Key == condKeyAWSPrincipalARN: + if gr.Conditions[i].Key == condKeyAWSPrincipalARN { gr.Conditions[i].Key = s3.PropertyKeyOwner val, err := formPrincipalOwner(gr.Conditions[i].Value, resolver) if err != nil { return GroupedConditions{}, err } gr.Conditions[i].Value = val - - case gr.Conditions[i].Key == condKeyAWSMFAPresent: - gr.Conditions[i].Key = s3.PropertyKeyAccessBoxAttrMFA - case strings.HasPrefix(gr.Conditions[i].Key, condKeyAWSResourceTagPrefix): - gr.Conditions[i].Kind = chain.KindResource } } @@ -220,52 +141,32 @@ func formPrincipalOwner(principal string, resolver S3Resolver) (string, error) { return address, nil } -func validateS3ResourceNames(names []string) error { +func formS3ResourceNames(names []string) ([]string, error) { + res := make([]string, len(names)) for i := range names { - if err := validateResource(names[i]); err != nil { - return err - } - } - - return nil -} - -func formS3ActionNames(names []string) ([]string, error) { - uniqueActions := make(map[string]struct{}, len(names)) - - for _, action := range names { - if action == Wildcard { - return []string{Wildcard}, nil - } - - isIAM, err := validateAction(action) + bkt, obj, err := parseResourceAsS3ARN(names[i]) if err != nil { return nil, err } - if isIAM { - uniqueActions[action] = struct{}{} + if bkt == Wildcard { + res[i] = bkt continue } - if action[len(s3ActionPrefix):] == Wildcard { - uniqueActions[action] = struct{}{} - continue - } - - s3Actions := actionToS3OpMap[action] - if len(s3Actions) == 0 { - return nil, ErrActionsNotApplicable - } - - for _, s3Action := range s3Actions { - uniqueActions[s3Action] = struct{}{} - } - } - - res := make([]string, 0, len(uniqueActions)) - for key := range uniqueActions { - res = append(res, key) + res[i] = bkt + "/" + obj + } + + return res, nil +} + +func formS3ActionNames(names []string) ([]string, error) { + var err error + res := make([]string, len(names)) + for i := range names { + if res[i], err = parseActionAsS3Action(names[i]); err != nil { + return nil, err + } } return res, nil diff --git a/iam/converter_test.go b/iam/converter_test.go index 55787fd..7ec177d 100644 --- a/iam/converter_test.go +++ b/iam/converter_test.go @@ -11,30 +11,28 @@ import ( "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/common" "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" "git.frostfs.info/TrueCloudLab/policy-engine/schema/s3" "github.com/stretchr/testify/require" ) type mockUserResolver struct { - users map[string]string - containers map[string]string - namespace string + users map[string]string + buckets map[string]string } -func newMockUserResolver(accountUsers []string, buckets []string, namespace string) *mockUserResolver { +func newMockUserResolver(accountUsers []string, buckets []string) *mockUserResolver { userMap := make(map[string]string, len(accountUsers)) for _, user := range accountUsers { userMap[user] = user + "/resolvedValue" } - containerMap := make(map[string]string, len(buckets)) + bucketMap := make(map[string]string, len(buckets)) for _, bkt := range buckets { - containerMap[bkt] = bkt + "/resolvedValues" + bucketMap[bkt] = bkt + "/resolvedValues" } - return &mockUserResolver{users: userMap, containers: containerMap, namespace: namespace} + return &mockUserResolver{users: userMap, buckets: bucketMap} } func (m *mockUserResolver) GetUserAddress(account, user string) (string, error) { @@ -55,13 +53,13 @@ func (m *mockUserResolver) GetUserKey(account, user string) (string, error) { return key, nil } -func (m *mockUserResolver) GetBucketInfo(bkt string) (*BucketInfo, error) { - cnr, ok := m.containers[bkt] +func (m *mockUserResolver) GetBucketCID(bkt string) (string, error) { + cnrID, ok := m.buckets[bkt] if !ok { - return nil, errors.New("not found") + return "", errors.New("not found") } - return &BucketInfo{Container: cnr, Namespace: m.namespace}, nil + return cnrID, nil } func TestConverters(t *testing.T) { @@ -71,11 +69,10 @@ func TestConverters(t *testing.T) { principal := "arn:aws:iam::" + namespace + ":user/" + userName bktName := "DOC-EXAMPLE-BUCKET" objName := "object-name" - resource := fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName) - s3GetObjectAction := "s3:GetObject" - s3HeadObjectAction := "s3:HeadObject" + resource := bktName + "/*" + action := "PutObject" - mockResolver := newMockUserResolver([]string{user}, []string{bktName}, namespace) + mockResolver := newMockUserResolver([]string{user}, []string{bktName}) t.Run("valid policy", func(t *testing.T) { p := Policy{ @@ -85,8 +82,8 @@ func TestConverters(t *testing.T) { AWSPrincipalType: {principal}, }, Effect: AllowEffect, - Action: []string{s3GetObjectAction}, - Resource: []string{resource}, + Action: []string{"s3:PutObject"}, + Resource: []string{"arn:aws:s3:::" + resource}, Conditions: map[string]Condition{ CondStringEquals: { "s3:RequestObjectTag/Department": {"Finance"}, @@ -98,20 +95,20 @@ func TestConverters(t *testing.T) { expected := &chain.Chain{Rules: []chain.Rule{ { Status: chain.Allow, - Actions: chain.Actions{Names: []string{s3GetObjectAction, s3HeadObjectAction}}, + Actions: chain.Actions{Names: []string{action}}, Resources: chain.Resources{Names: []string{resource}}, Condition: []chain.Condition{ { - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: s3.PropertyKeyOwner, - Value: mockResolver.users[user], + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: s3.PropertyKeyOwner, + Value: mockResolver.users[user], }, { - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: "s3:RequestObjectTag/Department", - Value: "Finance", + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: "s3:RequestObjectTag/Department", + Value: "Finance", }, }, }, @@ -119,7 +116,7 @@ func TestConverters(t *testing.T) { s3Chain, err := ConvertToS3Chain(p, mockResolver) require.NoError(t, err) - assertChainsEqual(t, expected, s3Chain) + require.Equal(t, expected, s3Chain) }) t.Run("valid native policy", func(t *testing.T) { @@ -131,25 +128,21 @@ func TestConverters(t *testing.T) { }, Effect: AllowEffect, Action: []string{"s3:PutObject"}, - Resource: []string{resource}, + Resource: []string{"arn:aws:s3:::" + resource}, }}, } expected := &chain.Chain{Rules: []chain.Rule{ { - Status: chain.Allow, - Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodPutObject, - native.MethodGetObject, native.MethodHeadObject, native.MethodRangeObject}}, - Resources: chain.Resources{Names: []string{ - fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName]), - fmt.Sprintf(native.ResourceFormatNamespaceContainer, namespace, mockResolver.containers[bktName])}, - }, + Status: chain.Allow, + Actions: chain.Actions{Names: []string{action}}, + Resources: chain.Resources{Names: []string{fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName])}}, Condition: []chain.Condition{ { - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: native.PropertyKeyActorPublicKey, - Value: mockResolver.users[user], + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: native.PropertyKeyActorPublicKey, + Value: mockResolver.users[user], }, }, }, @@ -157,7 +150,7 @@ func TestConverters(t *testing.T) { nativeChain, err := ConvertToNativeChain(p, mockResolver) require.NoError(t, err) - assertChainsEqual(t, expected, nativeChain) + require.Equal(t, expected, nativeChain) }) t.Run("valid inverted policy", func(t *testing.T) { @@ -168,22 +161,22 @@ func TestConverters(t *testing.T) { AWSPrincipalType: {principal}, }, Effect: DenyEffect, - NotAction: []string{s3GetObjectAction}, - NotResource: []string{resource}, + NotAction: []string{"s3:PutObject"}, + NotResource: []string{"arn:aws:s3:::" + resource}, }}, } expected := &chain.Chain{Rules: []chain.Rule{ { Status: chain.AccessDenied, - Actions: chain.Actions{Inverted: true, Names: []string{s3GetObjectAction, s3HeadObjectAction}}, + Actions: chain.Actions{Inverted: true, Names: []string{action}}, Resources: chain.Resources{Inverted: true, Names: []string{resource}}, Condition: []chain.Condition{ { - Op: chain.CondStringNotEquals, - Kind: chain.KindRequest, - Key: s3.PropertyKeyOwner, - Value: mockResolver.users[user], + Op: chain.CondStringNotEquals, + Object: chain.ObjectRequest, + Key: s3.PropertyKeyOwner, + Value: mockResolver.users[user], }, }, }, @@ -191,76 +184,49 @@ func TestConverters(t *testing.T) { s3Chain, err := ConvertToS3Chain(p, mockResolver) require.NoError(t, err) - assertChainsEqual(t, expected, s3Chain) + require.Equal(t, expected, s3Chain) }) - t.Run("valid native policy map action", func(t *testing.T) { + t.Run("valid policy map get action", func(t *testing.T) { p := Policy{ Version: "2012-10-17", Statement: []Statement{{ Principal: map[PrincipalType][]string{ AWSPrincipalType: {principal}, }, - Effect: AllowEffect, - Action: []string{"s3:DeleteObject", "s3:DeleteBucket"}, - Resource: []string{ - fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName, objName), - fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName), - }, + Effect: DenyEffect, + NotAction: []string{"s3:GetObject"}, + NotResource: []string{"arn:aws:s3:::" + bktName + "/" + objName}, }}, } expected := &chain.Chain{Rules: []chain.Rule{ { - Status: chain.Allow, - Actions: chain.Actions{Names: []string{ - native.MethodGetContainer, native.MethodDeleteContainer, - native.MethodSearchObject, native.MethodHeadObject, - native.MethodDeleteObject, native.MethodPutObject, - native.MethodGetObject, native.MethodRangeObject, - }}, - Resources: chain.Resources{Names: []string{ - fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName]), - fmt.Sprintf(native.ResourceFormatNamespaceContainer, namespace, mockResolver.containers[bktName]), + Status: chain.AccessDenied, + Actions: chain.Actions{Inverted: true, Names: actionToOpMap["GetObject"]}, + Resources: chain.Resources{Inverted: true, Names: []string{ + fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName]), }}, Condition: []chain.Condition{ { - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: native.PropertyKeyActorPublicKey, - Value: mockResolver.users[user], + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: native.PropertyKeyActorPublicKey, + Value: mockResolver.users[user], }, { - Op: chain.CondStringLike, - Kind: chain.KindResource, - Key: PropertyKeyFilePath, - Value: objName, + Op: chain.CondStringLike, + Object: chain.ObjectResource, + Key: PropertyKeyFilePath, + Value: objName, }, }, }, - { - Status: chain.Allow, - Actions: chain.Actions{Names: []string{ - native.MethodGetContainer, native.MethodDeleteContainer, - native.MethodSearchObject, native.MethodHeadObject, - native.MethodDeleteObject, native.MethodPutObject, - native.MethodGetObject, native.MethodRangeObject, - }}, - Resources: chain.Resources{Names: []string{ - fmt.Sprintf(native.ResourceFormatNamespaceContainer, namespace, mockResolver.containers[bktName]), - }}, - Condition: []chain.Condition{{ - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: native.PropertyKeyActorPublicKey, - Value: mockResolver.users[user], - }}, - }, }} nativeChain, err := ConvertToNativeChain(p, mockResolver) require.NoError(t, err) - assertChainsEqual(t, expected, nativeChain) + require.Equal(t, expected, nativeChain) }) t.Run("invalid policy (unsupported principal type)", func(t *testing.T) { @@ -312,66 +278,6 @@ func TestConverters(t *testing.T) { _, err := ConvertToNativeChain(p, mockResolver) 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{ - Version: "2012-10-17", - 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, - Kind: chain.KindRequest, - Key: s3.PropertyKeyOwner, - Value: mockResolver.users[user], - }}, - }}} - - s3Chain, err := ConvertToS3Chain(p, mockResolver) - require.NoError(t, err) - assertChainsEqual(t, s3Expected, s3Chain) - - nativeExpected := &chain.Chain{Rules: []chain.Rule{{ - Status: chain.Allow, - Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject}}, - Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects, native.ResourceFormatAllContainers}}, - Condition: []chain.Condition{{ - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: native.PropertyKeyActorPublicKey, - Value: mockResolver.users[user], - }}, - }}} - - nativeChain, err := ConvertToNativeChain(p, mockResolver) - require.NoError(t, err) - assertChainsEqual(t, nativeExpected, nativeChain) - }) } func TestConvertToChainCondition(t *testing.T) { @@ -391,16 +297,12 @@ func TestConvertToChainCondition(t *testing.T) { CondDateGreaterThan: {"key11": {"2006-01-02T15:04:05-01:00"}}, CondDateGreaterThanEquals: {"key12": {"2006-01-02T15:04:05-03:00"}}, CondBool: {"key13": {"True"}}, + CondIPAddress: {"key14": {"val14"}}, + CondNotIPAddress: {"key15": {"val15"}}, CondArnEquals: {"key16": {"val16"}}, CondArnLike: {condKeyAWSPrincipalARN: {principal}}, CondArnNotEquals: {"key18": {"val18"}}, CondArnNotLike: {"key19": {"val19"}}, - CondNumericEquals: {"key20": {"-20"}}, - CondNumericNotEquals: {"key21": {"+21"}}, - CondNumericLessThan: {"key22": {"0"}}, - CondNumericLessThanEquals: {"key23": {"23.23"}}, - CondNumericGreaterThan: {"key24": {"-24.24"}}, - CondNumericGreaterThanEquals: {"key25": {"+25.25"}}, } expectedCondition := []GroupedConditions{ @@ -408,193 +310,161 @@ func TestConvertToChainCondition(t *testing.T) { Any: true, Conditions: []chain.Condition{ { - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: "key1", - Value: "val0", + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: "key1", + Value: "val0", }, { - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: "key1", - Value: "val1", + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: "key1", + Value: "val1", }, }, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringNotEquals, - Kind: chain.KindRequest, - Key: "key2", - Value: "val2", + Op: chain.CondStringNotEquals, + Object: chain.ObjectRequest, + Key: "key2", + Value: "val2", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringEqualsIgnoreCase, - Kind: chain.KindRequest, - Key: "key3", - Value: "val3", + Op: chain.CondStringEqualsIgnoreCase, + Object: chain.ObjectRequest, + Key: "key3", + Value: "val3", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringNotEqualsIgnoreCase, - Kind: chain.KindRequest, - Key: "key4", - Value: "val4", + Op: chain.CondStringNotEqualsIgnoreCase, + Object: chain.ObjectRequest, + Key: "key4", + Value: "val4", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringLike, - Kind: chain.KindRequest, - Key: "key5", - Value: "val5", + Op: chain.CondStringLike, + Object: chain.ObjectRequest, + Key: "key5", + Value: "val5", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringNotLike, - Kind: chain.KindRequest, - Key: "key6", - Value: "val6", + Op: chain.CondStringNotLike, + Object: chain.ObjectRequest, + Key: "key6", + Value: "val6", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: "key7", - Value: "1136189045", + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: "key7", + Value: "1136189045", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringNotEquals, - Kind: chain.KindRequest, - Key: "key8", - Value: "1136214245", + Op: chain.CondStringNotEquals, + Object: chain.ObjectRequest, + Key: "key8", + Value: "1136214245", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringLessThan, - Kind: chain.KindRequest, - Key: "key9", - Value: "1136192645", + Op: chain.CondStringLessThan, + Object: chain.ObjectRequest, + Key: "key9", + Value: "1136192645", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringLessThanEquals, - Kind: chain.KindRequest, - Key: "key10", - Value: "1136203445", + Op: chain.CondStringLessThanEquals, + Object: chain.ObjectRequest, + Key: "key10", + Value: "1136203445", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringGreaterThan, - Kind: chain.KindRequest, - Key: "key11", - Value: "1136217845", + Op: chain.CondStringGreaterThan, + Object: chain.ObjectRequest, + Key: "key11", + Value: "1136217845", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringGreaterThanEquals, - Kind: chain.KindRequest, - Key: "key12", - Value: "1136225045", + Op: chain.CondStringGreaterThanEquals, + Object: chain.ObjectRequest, + Key: "key12", + Value: "1136225045", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringEqualsIgnoreCase, - Kind: chain.KindRequest, - Key: "key13", - Value: "True", + Op: chain.CondStringEqualsIgnoreCase, + Object: chain.ObjectRequest, + Key: "key13", + Value: "True", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: "key16", - Value: "val16", + Op: chain.CondStringLike, + Object: chain.ObjectRequest, + Key: "key14", + Value: "val14", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringLike, - Kind: chain.KindRequest, - Key: condKeyAWSPrincipalARN, - Value: principal, + Op: chain.CondStringNotLike, + Object: chain.ObjectRequest, + Key: "key15", + Value: "val15", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringNotEquals, - Kind: chain.KindRequest, - Key: "key18", - Value: "val18", + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: "key16", + Value: "val16", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondStringNotLike, - Kind: chain.KindRequest, - Key: "key19", - Value: "val19", + Op: chain.CondStringLike, + Object: chain.ObjectRequest, + Key: condKeyAWSPrincipalARN, + Value: principal, }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondNumericEquals, - Kind: chain.KindRequest, - Key: "key20", - Value: "-20", + Op: chain.CondStringNotEquals, + Object: chain.ObjectRequest, + Key: "key18", + Value: "val18", }}, }, { Conditions: []chain.Condition{{ - Op: chain.CondNumericNotEquals, - Kind: chain.KindRequest, - Key: "key21", - Value: "+21", - }}, - }, - { - Conditions: []chain.Condition{{ - Op: chain.CondNumericLessThan, - Kind: chain.KindRequest, - Key: "key22", - Value: "0", - }}, - }, - { - Conditions: []chain.Condition{{ - Op: chain.CondNumericLessThanEquals, - Kind: chain.KindRequest, - Key: "key23", - Value: "23.23", - }}, - }, - { - Conditions: []chain.Condition{{ - Op: chain.CondNumericGreaterThan, - Kind: chain.KindRequest, - Key: "key24", - Value: "-24.24", - }}, - }, - { - Conditions: []chain.Condition{{ - Op: chain.CondNumericGreaterThanEquals, - Kind: chain.KindRequest, - Key: "key25", - Value: "+25.25", + Op: chain.CondStringNotLike, + Object: chain.ObjectRequest, + Key: "key19", + Value: "val19", }}, }, } @@ -602,159 +472,6 @@ func TestConvertToChainCondition(t *testing.T) { actualCondition, err := convertToChainCondition(conditions) require.NoError(t, err) require.ElementsMatch(t, expectedCondition, actualCondition) - - invalidConditions := []Condition{ - {"key1": {"invalid"}}, - {"key2": {"1 2"}}, - {"key3": {"0x12f"}}, - {"key4": {"0b1010"}}, - {"key5": {"+Inf"}}, - {"key6": {"-Inf"}}, - {"key7": {"inf"}}, - {"key8": {"NaN"}}, - {"key9": {"nan"}}, - } - - for _, cond := range invalidConditions { - _, err = convertToChainCondition(Conditions{CondNumericEquals: cond}) - require.Error(t, err) - } -} - -func TestIPConditions(t *testing.T) { - t.Run("ip converters", func(t *testing.T) { - for _, tc := range []struct { - ip string - error bool - expected string - }{ - {ip: "203.0.113.0/24", expected: "203.0.113.0/24"}, - {ip: "203.0.113.1", expected: "203.0.113.1/32"}, - {ip: "203.0.113.1/", error: true}, - {ip: "203.0.113.1/33", error: true}, - {ip: "192.168.0.1/24", expected: "192.168.0.1/24"}, - {ip: "10.10.0.1/24", expected: "10.10.0.1/24"}, - {ip: "172.16.0.1/24", expected: "172.16.0.1/24"}, - {ip: "2001:DB8:1234:5678::/64", expected: "2001:DB8:1234:5678::/64"}, - {ip: "2001:DB8:1234:5678::", expected: "2001:DB8:1234:5678::/32"}, - {ip: "2001:DB8:1234:5678::/", error: true}, - {ip: "2001:DB8:1234:5678::/129", error: true}, - {ip: "FC00::/64", expected: "FC00::/64"}, - } { - t.Run("", func(t *testing.T) { - actual, err := ipConvertFunction(tc.ip) - if tc.error { - require.Error(t, err) - return - } - require.NoError(t, err) - require.Equal(t, tc.expected, actual) - }) - } - }) - - t.Run("chain converters", func(t *testing.T) { - for _, tc := range []struct { - name string - policy string - }{ - { - name: "wildcard principal", - policy: `{"Version":"2012-10-17", -"Statement":{"Effect":"Allow","Principal": "*","Action":"s3:*","Resource":"*","Condition": {"IpAddress": {"aws:SourceIp": "203.0.113.0/24"}}} -}`, - }, - { - name: "wildcard AWS principal", - policy: `{"Version":"2012-10-17", -"Statement":{"Effect":"Allow","Principal": {"AWS":"*"},"Action":"s3:*","Resource":"*","Condition": {"IpAddress": {"aws:SourceIp": "203.0.113.0/24"}}} -}`, - }, - } { - var p Policy - err := json.Unmarshal([]byte(tc.policy), &p) - require.NoError(t, err) - - s3Expected := &chain.Chain{ - Rules: []chain.Rule{{ - Status: chain.Allow, - Actions: chain.Actions{Names: []string{"s3:*"}}, - Resources: chain.Resources{Names: []string{Wildcard}}, - Condition: []chain.Condition{{ - Op: chain.CondIPAddress, - Kind: chain.KindRequest, - Key: common.PropertyKeyFrostFSSourceIP, - Value: "203.0.113.0/24", - }}, - }}, - } - - s3Chain, err := ConvertToS3Chain(p, newMockUserResolver(nil, nil, "")) - 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}}, - Condition: []chain.Condition{{ - Op: chain.CondIPAddress, - Kind: chain.KindRequest, - Key: common.PropertyKeyFrostFSSourceIP, - Value: "203.0.113.0/24", - }}, - }}, - } - - nativeChain, err := ConvertToNativeChain(p, newMockUserResolver(nil, nil, "")) - require.NoError(t, err) - require.Equal(t, nativeExpected, nativeChain) - } - }) - - t.Run("matching rules", func(t *testing.T) { - for _, tc := range []struct { - name string - policy string - }{ - { - name: "wildcard principal", - policy: `{"Version":"2012-10-17", -"Statement":{"Effect":"Allow","Principal": "*","Action":"s3:*","Resource":"*","Condition": {"IpAddress": {"aws:SourceIp": "203.0.113.0/24"}}} -}`, - }, - { - name: "wildcard AWS principal", - policy: `{"Version":"2012-10-17", -"Statement":{"Effect":"Allow","Principal": {"AWS":"*"},"Action":"s3:*","Resource":"*","Condition": {"IpAddress": {"aws:SourceIp": "203.0.113.0/24"}}} -}`, - }, - } { - var p Policy - err := json.Unmarshal([]byte(tc.policy), &p) - require.NoError(t, err) - - s3Chain, err := ConvertToS3Chain(p, newMockUserResolver(nil, nil, "")) - require.NoError(t, err) - - s := inmemory.NewInMemory() - _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(""), s3Chain) - require.NoError(t, err) - - req := testutil.NewRequest("s3:CreateBucket", testutil.NewResource(fmt.Sprintf(s3.ResourceFormatS3Bucket, "bkt"), nil), - map[string]string{common.PropertyKeyFrostFSSourceIP: "203.0.113.128"}) - status, _, err := s.IsAllowed(chain.S3, engine.NewRequestTargetWithNamespace(""), req) - require.NoError(t, err) - require.Equal(t, chain.Allow.String(), status.String()) - - req = testutil.NewRequest("s3:CreateBucket", testutil.NewResource(fmt.Sprintf(s3.ResourceFormatS3Bucket, "bkt"), nil), - map[string]string{common.PropertyKeyFrostFSSourceIP: "203.0.114.0"}) - status, _, err = s.IsAllowed(chain.S3, engine.NewRequestTargetWithNamespace(""), req) - require.NoError(t, err) - require.Equal(t, chain.NoRuleFound.String(), status.String()) - } - }) } func TestParsePrincipalARN(t *testing.T) { @@ -827,13 +544,10 @@ func TestComplexNativeConditions(t *testing.T) { key1, key2 := "key1", "key2" val0, val1, val2 := "val0", "val1", "val2" - mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3}, "") - nativeResource1 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName1]) - nativeResource1cnr := fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers[bktName1]) - nativeResource2 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName2]) - nativeResource2cnr := fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers[bktName2]) - nativeResource3 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName3]) - nativeResource3cnr := fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers[bktName3]) + mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3}) + nativeResource1 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName1]) + nativeResource2 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName2]) + nativeResource3 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName3]) p := Policy{ Version: "2012-10-17", @@ -841,7 +555,7 @@ func TestComplexNativeConditions(t *testing.T) { Principal: map[PrincipalType][]string{ AWSPrincipalType: {principal1, principal2}, }, - Effect: AllowEffect, + Effect: DenyEffect, Action: []string{"s3:" + action}, Resource: []string{"arn:aws:s3:::" + resource1, "arn:aws:s3:::" + resource2, "arn:aws:s3:::" + resource3}, Conditions: map[string]Condition{ @@ -851,17 +565,17 @@ func TestComplexNativeConditions(t *testing.T) { }}, } - expectedStatus := chain.Allow - expectedActions := chain.Actions{Names: actionToNativeOpMap["s3:"+action]} - expectedResource1 := chain.Resources{Names: []string{nativeResource1, nativeResource1cnr}} - expectedResource23 := chain.Resources{Names: []string{nativeResource2, nativeResource2cnr, nativeResource3, nativeResource3cnr}} + expectedStatus := chain.AccessDenied + expectedActions := chain.Actions{Names: actionToOpMap[action]} + expectedResource1 := chain.Resources{Names: []string{nativeResource1}} + expectedResource23 := chain.Resources{Names: []string{nativeResource2, nativeResource3}} - user1Condition := chain.Condition{Op: chain.CondStringEquals, Kind: chain.KindRequest, Key: native.PropertyKeyActorPublicKey, Value: mockResolver.users[user1]} - user2Condition := chain.Condition{Op: chain.CondStringEquals, Kind: chain.KindRequest, Key: native.PropertyKeyActorPublicKey, Value: mockResolver.users[user2]} - objectName1Condition := chain.Condition{Op: chain.CondStringLike, Kind: chain.KindResource, Key: PropertyKeyFilePath, Value: objName1} - key1val0Condition := chain.Condition{Op: chain.CondStringEquals, Kind: chain.KindRequest, Key: key1, Value: val0} - key1val1Condition := chain.Condition{Op: chain.CondStringEquals, Kind: chain.KindRequest, Key: key1, Value: val1} - key2val2Condition := chain.Condition{Op: chain.CondStringLike, Kind: chain.KindRequest, Key: key2, Value: val2} + user1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: native.PropertyKeyActorPublicKey, Value: mockResolver.users[user1]} + user2Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: native.PropertyKeyActorPublicKey, Value: mockResolver.users[user2]} + objectName1Condition := chain.Condition{Op: chain.CondStringLike, Object: chain.ObjectResource, Key: PropertyKeyFilePath, Value: objName1} + key1val0Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: key1, Value: val0} + key1val1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: key1, Value: val1} + key2val2Condition := chain.Condition{Op: chain.CondStringLike, Object: chain.ObjectRequest, Key: key2, Value: val2} expected := &chain.Chain{Rules: []chain.Rule{ { @@ -969,7 +683,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket resource1, all conditions matched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName2], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: "any-object-name", }, @@ -978,12 +692,12 @@ func TestComplexNativeConditions(t *testing.T) { key1: val0, key2: val2, }, - status: chain.Allow, + status: chain.AccessDenied, }, { name: "bucket resource3, all conditions matched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName3], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName3], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: "any-object-name", }, @@ -992,12 +706,12 @@ func TestComplexNativeConditions(t *testing.T) { key1: val0, key2: val2, }, - status: chain.Allow, + status: chain.AccessDenied, }, { name: "bucket resource, user condition mismatched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName2], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: "any-object-name", }, @@ -1010,7 +724,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket resource, key2 condition mismatched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName3], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName3], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: "any-object-name", }, @@ -1024,7 +738,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket resource, key1 condition mismatched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName2], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: "any-object-name", }, @@ -1037,7 +751,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket/object resource, all conditions matched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: objName1, }, @@ -1046,12 +760,12 @@ func TestComplexNativeConditions(t *testing.T) { key1: val0, key2: val2, }, - status: chain.Allow, + status: chain.AccessDenied, }, { name: "bucket/object resource, user condition mismatched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: objName1, }, @@ -1065,7 +779,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket/object resource, key1 condition mismatched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: objName1, }, @@ -1078,7 +792,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket/object resource, key2 condition mismatched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: objName1, }, @@ -1092,7 +806,7 @@ func TestComplexNativeConditions(t *testing.T) { { name: "bucket/object resource, object filepath condition mismatched", action: action, - resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[bktName1], "some-oid"), + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), resourceMap: map[string]string{ PropertyKeyFilePath: "any-object-name", }, @@ -1135,16 +849,15 @@ func TestComplexS3Conditions(t *testing.T) { principal2 := "arn:aws:iam::" + namespace + ":user/" + userName2 bktName1, bktName2, bktName3 := "bktName", "bktName2", "bktName3" objName1 := "objName1" - resource1 := fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName1, objName1) - resource2 := fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName2) - resource3 := fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName3) - action := "s3:DeleteObject" - action2 := "s3:DeleteMultipleObjects" + resource1 := bktName1 + "/" + objName1 + resource2 := bktName2 + "/*" + resource3 := bktName3 + "/*" + action := "PutObject" key1, key2 := "key1", "key2" 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{ Version: "2012-10-17", @@ -1153,8 +866,8 @@ func TestComplexS3Conditions(t *testing.T) { AWSPrincipalType: {principal1, principal2}, }, Effect: DenyEffect, - Action: []string{action}, - Resource: []string{resource1, resource2, resource3}, + Action: []string{"s3:" + action}, + Resource: []string{"arn:aws:s3:::" + resource1, "arn:aws:s3:::" + resource2, "arn:aws:s3:::" + resource3}, Conditions: map[string]Condition{ CondStringEquals: {key1: {val0, val1}}, CondStringLike: {key2: {val2}}, @@ -1163,14 +876,14 @@ func TestComplexS3Conditions(t *testing.T) { } expectedStatus := chain.AccessDenied - expectedActions := chain.Actions{Names: []string{action, action2}} + expectedActions := chain.Actions{Names: actionToOpMap[action]} expectedResources := chain.Resources{Names: []string{resource1, resource2, resource3}} - user1Condition := chain.Condition{Op: chain.CondStringEquals, Kind: chain.KindRequest, Key: s3.PropertyKeyOwner, Value: mockResolver.users[user1]} - user2Condition := chain.Condition{Op: chain.CondStringEquals, Kind: chain.KindRequest, Key: s3.PropertyKeyOwner, Value: mockResolver.users[user2]} - key1val0Condition := chain.Condition{Op: chain.CondStringEquals, Kind: chain.KindRequest, Key: key1, Value: val0} - key1val1Condition := chain.Condition{Op: chain.CondStringEquals, Kind: chain.KindRequest, Key: key1, Value: val1} - key2val2Condition := chain.Condition{Op: chain.CondStringLike, Kind: chain.KindRequest, Key: key2, Value: val2} + user1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: s3.PropertyKeyOwner, Value: mockResolver.users[user1]} + user2Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: s3.PropertyKeyOwner, Value: mockResolver.users[user2]} + key1val0Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: key1, Value: val0} + key1val1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: key1, Value: val1} + key2val2Condition := chain.Condition{Op: chain.CondStringLike, Object: chain.ObjectRequest, Key: key2, Value: val2} expected := &chain.Chain{Rules: []chain.Rule{ { @@ -1245,7 +958,7 @@ func TestComplexS3Conditions(t *testing.T) { { name: "bucket resource3, all conditions matched", action: action, - resource: fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName3, "some-obj"), + resource: bktName3 + "/some-obj", requestMap: map[string]string{ s3.PropertyKeyOwner: mockResolver.users[user1], key1: val0, @@ -1256,7 +969,7 @@ func TestComplexS3Conditions(t *testing.T) { { name: "bucket resource, user condition mismatched", action: action, - resource: fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName2, "some-obj"), + resource: bktName2 + "/some-obj", requestMap: map[string]string{ key1: val0, key2: val2, @@ -1266,7 +979,7 @@ func TestComplexS3Conditions(t *testing.T) { { name: "bucket resource, key2 condition mismatched", action: action, - resource: fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName3, "some-obj"), + resource: bktName3 + "/some-obj", requestMap: map[string]string{ s3.PropertyKeyOwner: mockResolver.users[user1], key1: val0, @@ -1277,7 +990,7 @@ func TestComplexS3Conditions(t *testing.T) { { name: "bucket resource, key1 condition mismatched", action: action, - resource: fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName2, "some-obj"), + resource: bktName2 + "/some-obj", requestMap: map[string]string{ s3.PropertyKeyOwner: mockResolver.users[user1], key2: val2, @@ -1298,7 +1011,7 @@ func TestComplexS3Conditions(t *testing.T) { { name: "bucket/object resource, resource mismatched", action: action, - resource: fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName1, "some-obj"), + resource: bktName1 + "/some-obj", requestMap: map[string]string{ s3.PropertyKeyOwner: mockResolver.users[user1], key1: val0, @@ -1341,7 +1054,7 @@ func TestComplexS3Conditions(t *testing.T) { { name: "resource mismatched", action: action, - resource: fmt.Sprintf(s3.ResourceFormatS3BucketObject, "some-bkt", "some-obj"), + resource: "some-bkt/some-obj", requestMap: map[string]string{ s3.PropertyKeyOwner: mockResolver.users[user1], key1: val0, @@ -1359,313 +1072,71 @@ func TestComplexS3Conditions(t *testing.T) { } } -func TestS3BucketResource(t *testing.T) { - namespace := "ns" - bktName1, bktName2 := "bucket1", "bucket2" - chainName := chain.Name("name") +func TestWildcardConverters(t *testing.T) { + policy := `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"*","Resource":"*"}}` - mockResolver := newMockUserResolver([]string{}, []string{}, "") - - p := Policy{ - Version: "2012-10-17", - Statement: []Statement{ - { - Principal: map[PrincipalType][]string{Wildcard: nil}, - Effect: DenyEffect, - Action: []string{"s3:ListBucket"}, - 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) + var p Policy + err := json.Unmarshal([]byte(policy), &p) require.NoError(t, err) - s := inmemory.NewInMemory() - _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chainName, engine.NamespaceTarget(namespace), s3Chain) + _, err = ConvertToS3Chain(p, newMockUserResolver(nil, nil)) 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) + _, err = ConvertToNativeChain(p, newMockUserResolver(nil, nil)) 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 TestActionParsing(t *testing.T) { for _, tc := range []struct { - name string - policy string + action string + expected string + err bool }{ { - name: "wildcard principal", - policy: `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"s3:*","Resource":"*"}}`, + action: "withoutPrefix", + expected: "", + err: true, }, { - name: "wildcard AWS principal", - policy: `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": {"AWS":"*"}, "Action":"s3:*","Resource":"*"}}`, + action: "s3:*Object", + expected: "", + err: true, + }, + { + action: "*", + expected: "*", + }, + { + action: "s3:PutObject", + expected: "PutObject", + }, + { + action: "s3:Put*", + expected: "Put*", + }, + { + action: "s3:*", + expected: "*", + }, + { + action: "s3:", + + expected: "", }, } { - t.Run(tc.name, func(t *testing.T) { - var p Policy - err := json.Unmarshal([]byte(tc.policy), &p) - require.NoError(t, err) - - s3Expected := &chain.Chain{ - Rules: []chain.Rule{{ - Status: chain.Allow, - Actions: chain.Actions{Names: []string{"s3:*"}}, - Resources: chain.Resources{Names: []string{Wildcard}}, - }}, + t.Run("", func(t *testing.T) { + actual, err := parseActionAsS3Action(tc.action) + if tc.err { + require.Error(t, err) + return } - s3Chain, err := ConvertToS3Chain(p, newMockUserResolver(nil, nil, "")) 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) + require.Equal(t, tc.expected, actual) }) } } -func TestWildcardObjectsConverters(t *testing.T) { - for _, tc := range []struct { - name string - policy string - }{ - { - name: "wildcard principal", - policy: `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"s3:*","Resource":"arn:aws:s3:::bucket/*"}}`, - }, - { - name: "wildcard AWS principal", - policy: `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": {"AWS":"*"}, "Action":"s3:*","Resource":"arn:aws:s3:::bucket/*"}}`, - }, - } { - var p Policy - err := json.Unmarshal([]byte(tc.policy), &p) - require.NoError(t, err) - - s3Expected := &chain.Chain{ - Rules: []chain.Rule{{ - Status: chain.Allow, - Actions: chain.Actions{Names: []string{"s3:*"}}, - Resources: chain.Resources{Names: []string{"arn:aws:s3:::bucket/*"}}, - }}, - } - - s3Chain, err := ConvertToS3Chain(p, newMockUserResolver(nil, nil, "")) - require.NoError(t, err) - require.Equal(t, s3Expected, s3Chain) - - mockResolver := newMockUserResolver(nil, []string{"bucket"}, "") - - nativeExpected := &chain.Chain{ - Rules: []chain.Rule{{ - Status: chain.Allow, - Actions: chain.Actions{Names: []string{Wildcard}}, - Resources: chain.Resources{Names: []string{ - fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers["bucket"]), - fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers["bucket"]), - }}, - }}, - } - - nativeChain, err := ConvertToNativeChain(p, mockResolver) - require.NoError(t, err) - assertChainsEqual(t, nativeExpected, nativeChain) - } -} - -func TestDisableNativeDeny(t *testing.T) { - for _, tc := range []struct { - name string - policy string - }{ - { - name: "wildcard principal", - policy: ` -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Deny", - "Principal": "*", - "Action": "s3:*", - "Resource": [ "arn:aws:s3:::test-bucket/*" ] - } - ] -} -`, - }, - { - name: "wildcard AWS principal", - policy: ` -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Deny", - "Principal": {"AWS":"*"}, - "Action": "s3:*", - "Resource": [ "arn:aws:s3:::test-bucket/*" ] - } - ] -} -`, - }, - } { - var p Policy - err := json.Unmarshal([]byte(tc.policy), &p) - require.NoError(t, err) - - _, err = ConvertToNativeChain(p, newMockUserResolver(nil, nil, "")) - require.ErrorIs(t, err, ErrActionsNotApplicable) - } -} - -func TestFromActions(t *testing.T) { - t.Run("s3 actions", func(t *testing.T) { - for _, tc := range []struct { - action string - res []string - err bool - }{ - { - action: "withoutPrefix", - err: true, - }, - { - action: "s3:*Object", - err: true, - }, - { - action: "*", - res: []string{Wildcard}, - }, - { - action: "s3:PutObject", - res: []string{"s3:PutObject", "s3:PostObject", "s3:CopyObject", - "s3:UploadPart", "s3:UploadPartCopy", "s3:CreateMultipartUpload", "s3:CompleteMultipartUpload"}, - }, - { - action: "s3:Put*", - err: true, - }, - { - action: "s3:*", - res: []string{"s3:*"}, - }, - { - action: "s3:", - err: true, - }, - { - action: "iam:ListAccessKeys", - res: []string{"iam:ListAccessKeys"}, - }, - { - action: "iam:*", - res: []string{"iam:*"}, - }, - } { - t.Run("", func(t *testing.T) { - actions, err := formS3ActionNames([]string{tc.action}) - if tc.err { - require.Error(t, err) - } else { - require.NoError(t, err) - require.ElementsMatch(t, tc.res, actions) - } - }) - } - }) - - t.Run("native actions", func(t *testing.T) { - for _, tc := range []struct { - action string - res []string - err bool - }{ - { - action: "withoutPrefix", - err: true, - }, - { - action: "s3:*Object", - err: true, - }, - { - action: "*", - res: []string{Wildcard}, - }, - { - action: "s3:PutObject", - res: []string{native.MethodGetContainer, native.MethodPutObject, - native.MethodGetObject, native.MethodHeadObject, native.MethodRangeObject}, - }, - { - action: "s3:Put*", - err: true, - }, - { - action: "s3:*", - res: []string{Wildcard}, - }, - { - action: "s3:", - err: true, - }, - { - action: "iam:ListAccessKeys", - res: []string{}, - }, - { - action: "iam:*", - res: []string{}, - }, - } { - t.Run("", func(t *testing.T) { - actions, err := formNativeActionNames([]string{tc.action}) - if tc.err { - require.Error(t, err) - } else { - require.NoError(t, err) - require.ElementsMatch(t, tc.res, actions) - } - }) - } - }) -} - func TestPrincipalParsing(t *testing.T) { for _, tc := range []struct { principal string @@ -1731,244 +1202,59 @@ func TestPrincipalParsing(t *testing.T) { func TestResourceParsing(t *testing.T) { for _, tc := range []struct { - resource string - err bool + resource string + expectedBucket string + expectedObject string + err bool }{ - {resource: "withoutPrefixAnd", err: true}, - {resource: "arn:aws:s3:::*/obj", err: true}, - {resource: "arn:aws:s3:::bkt/*"}, - {resource: "arn:aws:s3:::bkt"}, - {resource: "arn:aws:s3:::bkt/"}, - {resource: "arn:aws:s3:::*"}, - {resource: "*"}, + { + resource: "withoutPrefixAnd", + err: true, + }, + { + resource: "arn:aws:s3:::*/obj", + err: true, + }, + { + resource: "arn:aws:s3:::bkt/*", + expectedBucket: "bkt", + expectedObject: "*", + }, + { + resource: "arn:aws:s3:::bkt", + expectedBucket: "bkt", + expectedObject: "*", + }, + { + resource: "arn:aws:s3:::bkt/", + expectedBucket: "bkt", + expectedObject: "*", + }, + { + resource: "arn:aws:s3:::*", + expectedBucket: "*", + expectedObject: "*", + }, + { + resource: "*", + expectedBucket: "*", + expectedObject: "*", + }, } { t.Run("", func(t *testing.T) { - err := validateResource(tc.resource) + bkt, obj, err := parseResourceAsS3ARN(tc.resource) if tc.err { require.Error(t, err) - } else { - require.NoError(t, err) + return } + + require.NoError(t, err) + require.Equal(t, tc.expectedBucket, bkt) + require.Equal(t, tc.expectedObject, obj) }) } } -func TestTagsConditions(t *testing.T) { - expectedS3Conditions := []chain.Condition{ - { - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, "tag-department"), - Value: "hr", - }, - { - Op: chain.CondStringEquals, - Kind: chain.KindResource, - Key: fmt.Sprintf(s3.PropertyKeyFormatResourceTag, "owner"), - Value: "hr-admin", - }, - { - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: fmt.Sprintf(s3.PropertyKeyFormatRequestTag, "scope"), - Value: "*", - }, - } - - expectedNativeConditions := []chain.Condition{ - { - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, "tag-department"), - Value: "hr", - }, - } - - for _, tc := range []struct { - name string - policy string - }{ - { - name: "wildcard principal", - policy: ` -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": "*", - "Action": "s3:PutObjectTagging", - "Resource": "*", - "Condition": { - "StringEquals": { - "aws:PrincipalTag/department": "hr", - "aws:ResourceTag/owner": "hr-admin", - "aws:RequestTag/scope": "*" - } - } - } - ] -} -`, - }, - { - name: "wildcard AWS principal", - policy: ` -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": {"AWS":"*"}, - "Action": "s3:PutObjectTagging", - "Resource": "*", - "Condition": { - "StringEquals": { - "aws:PrincipalTag/department": "hr", - "aws:ResourceTag/owner": "hr-admin", - "aws:RequestTag/scope": "*" - } - } - } - ] -} -`, - }, - } { - var p Policy - err := json.Unmarshal([]byte(tc.policy), &p) - require.NoError(t, err) - - s3Chain, err := ConvertToS3Chain(p, newMockUserResolver(nil, nil, "")) - require.NoError(t, err) - require.Len(t, s3Chain.Rules, 1) - require.ElementsMatch(t, expectedS3Conditions, s3Chain.Rules[0].Condition) - - nativeChain, err := ConvertToNativeChain(p, newMockUserResolver(nil, nil, "")) - require.NoError(t, err) - require.Len(t, nativeChain.Rules, 1) - require.ElementsMatch(t, expectedNativeConditions, nativeChain.Rules[0].Condition) - } -} - -func TestMFACondition(t *testing.T) { - for _, tc := range []struct { - name string - policy string - expectedValue string - }{ - { - name: "wildcard principal, mfa present true", - policy: ` -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": "*", - "Action": "s3:PutObject", - "Resource": "*", - "Condition": { - "Bool": { - "aws:MultiFactorAuthPresent": "true" - } - } - } - ] -} -`, - expectedValue: "true", - }, - { - name: "wildcard AWS principal, mfa present true", - policy: ` -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": {"AWS":"*"}, - "Action": "s3:PutObject", - "Resource": "*", - "Condition": { - "Bool": { - "aws:MultiFactorAuthPresent": "true" - } - } - } - ] -} -`, - expectedValue: "true", - }, - { - name: "wildcard principal, mfa present false", - policy: ` -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": "*", - "Action": "s3:GetObject", - "Resource": "*", - "Condition": { - "Bool": { - "aws:MultiFactorAuthPresent": "false" - } - } - } - ] -} -`, - expectedValue: "false", - }, - { - name: "wildcard AWS principal, mfa present false", - policy: ` -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": {"AWS":"*"}, - "Action": "s3:GetObject", - "Resource": "*", - "Condition": { - "Bool": { - "aws:MultiFactorAuthPresent": "false" - } - } - } - ] -} -`, - expectedValue: "false", - }, - } { - expectedConditions := []chain.Condition{ - { - Op: chain.CondStringEqualsIgnoreCase, - Kind: chain.KindRequest, - Key: s3.PropertyKeyAccessBoxAttrMFA, - Value: tc.expectedValue, - }, - } - - var p Policy - err := json.Unmarshal([]byte(tc.policy), &p) - require.NoError(t, err) - - s3Chain, err := ConvertToS3Chain(p, newMockUserResolver(nil, nil, "")) - require.NoError(t, err) - require.Len(t, s3Chain.Rules, 1) - require.ElementsMatch(t, expectedConditions, s3Chain.Rules[0].Condition) - - _, err = ConvertToNativeChain(p, newMockUserResolver(nil, nil, "")) - require.ErrorIs(t, err, ErrActionsNotApplicable) - } -} - func requireChainRulesMatch(t *testing.T, expected, actual []chain.Rule) { require.Equal(t, len(expected), len(actual), "length of chain rules differ") @@ -1999,39 +1285,19 @@ func areRulesMatched(rule1, rule2 chain.Rule) bool { return false } + for i, name := range rule1.Resources.Names { + if name != rule2.Resources.Names[i] { + return false + } + } + + for i, name := range rule1.Actions.Names { + if name != rule2.Actions.Names[i] { + return false + } + } + seen := make(map[int]struct{}) - for _, name1 := range rule1.Resources.Names { - for j, name2 := range rule2.Resources.Names { - if _, ok := seen[j]; ok { - continue - } - if name1 == name2 { - seen[j] = struct{}{} - break - } - } - } - if len(seen) != len(rule1.Resources.Names) { - return false - } - - seen = make(map[int]struct{}) - for _, name1 := range rule1.Actions.Names { - for j, name2 := range rule2.Actions.Names { - if _, ok := seen[j]; ok { - continue - } - if name1 == name2 { - seen[j] = struct{}{} - break - } - } - } - if len(seen) != len(rule1.Actions.Names) { - return false - } - - seen = make(map[int]struct{}) for _, cond1 := range rule1.Condition { for j, cond2 := range rule2.Condition { if _, ok := seen[j]; ok { @@ -2046,19 +1312,3 @@ func areRulesMatched(rule1, rule2 chain.Rule) bool { return len(seen) == len(rule1.Condition) } - -func assertChainsEqual(t *testing.T, chain1, chain2 *chain.Chain) { - require.Equal(t, string(chain1.ID), string(chain2.ID)) - require.Equal(t, chain1.MatchType, chain2.MatchType) - require.Equal(t, len(chain1.Rules), len(chain2.Rules)) - - for i, rule := range chain1.Rules { - require.Equal(t, rule.Any, chain2.Rules[i].Any) - require.Equal(t, rule.Resources.Inverted, chain2.Rules[i].Resources.Inverted) - require.ElementsMatch(t, rule.Resources.Names, chain2.Rules[i].Resources.Names) - require.Equal(t, rule.Status, chain2.Rules[i].Status) - require.ElementsMatch(t, rule.Condition, chain2.Rules[i].Condition) - require.Equal(t, rule.Actions.Inverted, chain2.Rules[i].Actions.Inverted) - require.ElementsMatch(t, rule.Actions.Names, chain2.Rules[i].Actions.Names) - } -} diff --git a/iam/policy.go b/iam/policy.go index e38bb81..d5649fa 100644 --- a/iam/policy.go +++ b/iam/policy.go @@ -46,8 +46,6 @@ type ( PrincipalType string ) -const policyVersion = "2012-10-17" - const ( GeneralPolicyType PolicyType = iota IdentityBasedPolicyType @@ -112,15 +110,6 @@ func (p *Principal) UnmarshalJSON(data []byte) error { return err } - val, ok := m[AWSPrincipalType] - if ok { - str, ok = val.(string) - if ok && str == Wildcard && len(m) == 1 { - (*p)[Wildcard] = nil - return nil - } - } - for key, val := range m { element, ok := val.(string) if ok { @@ -233,20 +222,11 @@ func (p Policy) Validate(typ PolicyType) error { } func (p Policy) validate() error { - if p.Version != policyVersion { - return fmt.Errorf("invalid policy version, expected '%s', actual: '%s'", policyVersion, p.Version) - } - if len(p.Statement) == 0 { return errors.New("'Statement' is missing") } - sids := make(map[string]struct{}, len(p.Statement)) for _, statement := range p.Statement { - if _, ok := sids[statement.SID]; ok && statement.SID != "" { - return fmt.Errorf("duplicate 'SID': %s", statement.SID) - } - sids[statement.SID] = struct{}{} if !statement.Effect.IsValid() { return fmt.Errorf("unknown effect: '%s'", statement.Effect) } diff --git a/iam/policy_test.go b/iam/policy_test.go index 79d93c6..c397f33 100644 --- a/iam/policy_test.go +++ b/iam/policy_test.go @@ -2,13 +2,14 @@ package iam import ( "encoding/json" + "fmt" "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/s3" + "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" "github.com/stretchr/testify/require" ) @@ -217,7 +218,6 @@ func TestValidatePolicies(t *testing.T) { { name: "valid permission boundaries", policy: Policy{ - Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, @@ -230,7 +230,6 @@ func TestValidatePolicies(t *testing.T) { { name: "general invalid effect", policy: Policy{ - Version: policyVersion, Statement: []Statement{{ Effect: "dummy", Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, @@ -243,7 +242,6 @@ func TestValidatePolicies(t *testing.T) { { name: "general invalid principal block", policy: Policy{ - Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, @@ -258,7 +256,6 @@ func TestValidatePolicies(t *testing.T) { { name: "general invalid not principal", policy: Policy{ - Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, @@ -272,7 +269,6 @@ func TestValidatePolicies(t *testing.T) { { name: "general invalid principal type", policy: Policy{ - Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, @@ -286,7 +282,6 @@ func TestValidatePolicies(t *testing.T) { { name: "general invalid action block", policy: Policy{ - Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, @@ -300,7 +295,6 @@ func TestValidatePolicies(t *testing.T) { { name: "general invalid resource block", policy: Policy{ - Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Resource: []string{Wildcard}, @@ -313,7 +307,6 @@ func TestValidatePolicies(t *testing.T) { { name: "invalid resource block", policy: Policy{ - Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Resource: []string{}, @@ -326,7 +319,6 @@ func TestValidatePolicies(t *testing.T) { { name: "missing resource block", policy: Policy{ - Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, }}, @@ -340,43 +332,9 @@ func TestValidatePolicies(t *testing.T) { typ: GeneralPolicyType, isValid: false, }, - { - name: "duplicate sid", - policy: Policy{ - Version: policyVersion, - Statement: []Statement{ - { - SID: "sid", - Effect: AllowEffect, - Action: []string{"s3:*"}, - Resource: []string{Wildcard}, - }, - { - SID: "sid", - Effect: AllowEffect, - Action: []string{"cloudwatch:*"}, - Resource: []string{Wildcard}, - }}, - }, - typ: GeneralPolicyType, - isValid: false, - }, - { - name: "missing version", - policy: Policy{ - Statement: []Statement{{ - Effect: AllowEffect, - Action: []string{"s3:*"}, - Resource: []string{Wildcard}, - }}, - }, - typ: GeneralPolicyType, - isValid: false, - }, { name: "identity based valid", policy: Policy{ - Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:PutObject"}, @@ -389,8 +347,7 @@ func TestValidatePolicies(t *testing.T) { { name: "identity based invalid because of id presence", policy: Policy{ - ID: "some-id", - Version: policyVersion, + ID: "some-id", Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:PutObject"}, @@ -403,7 +360,6 @@ func TestValidatePolicies(t *testing.T) { { name: "identity based invalid because of principal presence", policy: Policy{ - Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:PutObject"}, @@ -417,7 +373,6 @@ func TestValidatePolicies(t *testing.T) { { name: "identity based invalid because of not principal presence", policy: Policy{ - Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:PutObject"}, @@ -431,7 +386,6 @@ func TestValidatePolicies(t *testing.T) { { name: "resource based valid principal", policy: Policy{ - Version: policyVersion, Statement: []Statement{{ Effect: DenyEffect, Action: []string{"s3:PutObject"}, @@ -445,8 +399,7 @@ func TestValidatePolicies(t *testing.T) { { name: "resource based valid not principal", policy: Policy{ - ID: "some-id", - Version: policyVersion, + ID: "some-id", Statement: []Statement{{ Effect: DenyEffect, Action: []string{"s3:PutObject"}, @@ -460,8 +413,7 @@ func TestValidatePolicies(t *testing.T) { { name: "resource based invalid missing principal", policy: Policy{ - ID: "some-id", - Version: policyVersion, + ID: "some-id", Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:PutObject"}, @@ -522,29 +474,29 @@ func TestProcessDenyFirst(t *testing.T) { err = json.Unmarshal([]byte(resourceBasedPolicyStr), &resourcePolicy) require.NoError(t, err) - mockResolver := newMockUserResolver([]string{"root/user-name"}, []string{"test-bucket"}, "") + mockResolver := newMockUserResolver([]string{"root/user-name"}, []string{"test-bucket"}) - identityNativePolicy, err := ConvertToS3Chain(identityPolicy, mockResolver) + identityNativePolicy, err := ConvertToNativeChain(identityPolicy, mockResolver) require.NoError(t, err) identityNativePolicy.MatchType = chain.MatchTypeFirstMatch - resourceNativePolicy, err := ConvertToS3Chain(resourcePolicy, mockResolver) + resourceNativePolicy, err := ConvertToNativeChain(resourcePolicy, mockResolver) require.NoError(t, err) s := inmemory.NewInMemory() target := engine.NamespaceTarget("ns") - _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, target, identityNativePolicy) + _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, identityNativePolicy) require.NoError(t, err) - _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, target, resourceNativePolicy) + _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, resourceNativePolicy) require.NoError(t, err) - resource := testutil.NewResource("arn:aws:s3:::test-bucket/object", nil) - request := testutil.NewRequest("s3:PutObject", resource, map[string]string{s3.PropertyKeyOwner: mockResolver.users["root/user-name"]}) + resource := testutil.NewResource(fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets["test-bucket"]), nil) + request := testutil.NewRequest("PutObject", resource, map[string]string{native.PropertyKeyActorPublicKey: mockResolver.users["root/user-name"]}) - status, found, err := s.IsAllowed(chain.S3, engine.NewRequestTarget("ns", ""), request) + 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) diff --git a/pkg/chain/chain.go b/pkg/chain/chain.go index 5e94940..d241ca8 100644 --- a/pkg/chain/chain.go +++ b/pkg/chain/chain.go @@ -1,18 +1,16 @@ package chain import ( + "encoding/json" "fmt" - "net/netip" "strings" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" "git.frostfs.info/TrueCloudLab/policy-engine/util" - "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" - "golang.org/x/exp/slices" ) // ID is the ID of rule chain. -type ID []byte +type ID string // MatchType is the match type for chain rules. type MatchType uint8 @@ -24,7 +22,6 @@ const ( MatchTypeFirstMatch MatchType = 1 ) -//easyjson:json type Chain struct { ID ID @@ -34,7 +31,7 @@ type Chain struct { } func (c *Chain) Bytes() []byte { - data, err := c.MarshalBinary() + data, err := json.Marshal(c) if err != nil { panic(err) } @@ -42,7 +39,7 @@ func (c *Chain) Bytes() []byte { } func (c *Chain) DecodeBytes(b []byte) error { - return c.UnmarshalBinary(b) + return json.Unmarshal(b, c) } type Rule struct { @@ -68,17 +65,17 @@ type Resources struct { } type Condition struct { - Op ConditionType - Kind ConditionKindType - Key string - Value string + Op ConditionType + Object ObjectType + Key string + Value string } -type ConditionKindType byte +type ObjectType byte const ( - KindResource ConditionKindType = iota - KindRequest + ObjectResource ObjectType = iota + ObjectRequest ) type ConditionType byte @@ -106,64 +103,56 @@ const ( CondNumericLessThanEquals CondNumericGreaterThan CondNumericGreaterThanEquals - - CondSliceContains - - CondIPAddress - CondNotIPAddress ) -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"}, - {CondIPAddress, "IPAddress"}, - {CondNotIPAddress, "NotIPAddress"}, -} - func (c ConditionType) String() string { - for _, v := range condToStr { - if v.ct == c { - return v.str - } + switch c { + case CondStringEquals: + return "StringEquals" + 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 { var val string - switch c.Kind { - case KindResource: + switch c.Object { + case ObjectResource: val = req.Resource().Property(c.Key) - case KindRequest: + case ObjectRequest: val = req.Property(c.Key) default: - panic(fmt.Sprintf("unknown condition type: %d", c.Kind)) + panic(fmt.Sprintf("unknown condition type: %d", c.Object)) } switch c.Op { @@ -189,63 +178,6 @@ func (c *Condition) Match(req resource.Request) bool { return val > c.Value case CondStringGreaterThanEquals: return val >= c.Value - case CondSliceContains: - return slices.Contains(strings.Split(val, condSliceContainsDelimiter), c.Value) - case CondNumericEquals, CondNumericNotEquals, CondNumericLessThan, CondNumericLessThanEquals, CondNumericGreaterThan, - CondNumericGreaterThanEquals: - return c.matchNumeric(val) - case CondIPAddress, CondNotIPAddress: - return c.matchIP(val) - } -} - -func (c *Condition) matchNumeric(val string) bool { - valDecimal, err := fixedn.Fixed8FromString(val) - if err != nil { - return c.Op == CondNumericNotEquals - } - - condVal, err := fixedn.Fixed8FromString(c.Value) - if err != nil { - return c.Op == CondNumericNotEquals - } - - switch c.Op { - default: - panic(fmt.Sprintf("unimplemented: %d", c.Op)) - case CondNumericEquals: - return valDecimal.Equal(condVal) - case CondNumericNotEquals: - return !valDecimal.Equal(condVal) - case CondNumericLessThan: - return valDecimal.LessThan(condVal) - case CondNumericLessThanEquals: - return valDecimal.LessThan(condVal) || valDecimal.Equal(condVal) - case CondNumericGreaterThan: - return valDecimal.GreaterThan(condVal) - case CondNumericGreaterThanEquals: - return valDecimal.GreaterThan(condVal) || valDecimal.Equal(condVal) - } -} - -func (c *Condition) matchIP(val string) bool { - ipAddr, err := netip.ParseAddr(val) - if err != nil { - return false - } - - prefix, err := netip.ParsePrefix(c.Value) - if err != nil { - return false - } - - switch c.Op { - default: - panic(fmt.Sprintf("unimplemented: %d", c.Op)) - case CondIPAddress: - return prefix.Contains(ipAddr) - case CondNotIPAddress: - return !prefix.Contains(ipAddr) } } diff --git a/pkg/chain/chain_easyjson.go b/pkg/chain/chain_easyjson.go deleted file mode 100644 index bf94c02..0000000 --- a/pkg/chain/chain_easyjson.go +++ /dev/null @@ -1,463 +0,0 @@ -// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. - -package chain - -import ( - json "encoding/json" - easyjson "github.com/mailru/easyjson" - jlexer "github.com/mailru/easyjson/jlexer" - jwriter "github.com/mailru/easyjson/jwriter" -) - -// suppress unused package warning -var ( - _ *json.RawMessage - _ *jlexer.Lexer - _ *jwriter.Writer - _ easyjson.Marshaler -) - -func easyjsonE2758465DecodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain(in *jlexer.Lexer, out *Chain) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - if in.IsNull() { - in.Skip() - in.WantComma() - continue - } - switch key { - case "ID": - if in.IsNull() { - in.Skip() - out.ID = nil - } else { - out.ID = in.Bytes() - } - case "Rules": - if in.IsNull() { - in.Skip() - out.Rules = nil - } else { - in.Delim('[') - if out.Rules == nil { - if !in.IsDelim(']') { - out.Rules = make([]Rule, 0, 0) - } else { - out.Rules = []Rule{} - } - } else { - out.Rules = (out.Rules)[:0] - } - for !in.IsDelim(']') { - var v2 Rule - easyjsonE2758465DecodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain1(in, &v2) - out.Rules = append(out.Rules, v2) - in.WantComma() - } - in.Delim(']') - } - case "MatchType": - (out.MatchType).UnmarshalEasyJSON(in) - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonE2758465EncodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain(out *jwriter.Writer, in Chain) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"ID\":" - out.RawString(prefix[1:]) - out.Base64Bytes(in.ID) - } - { - const prefix string = ",\"Rules\":" - out.RawString(prefix) - if in.Rules == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { - out.RawString("null") - } else { - out.RawByte('[') - for v5, v6 := range in.Rules { - if v5 > 0 { - out.RawByte(',') - } - easyjsonE2758465EncodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain1(out, v6) - } - out.RawByte(']') - } - } - { - const prefix string = ",\"MatchType\":" - out.RawString(prefix) - (in.MatchType).MarshalEasyJSON(out) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v Chain) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonE2758465EncodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v Chain) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonE2758465EncodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *Chain) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonE2758465DecodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *Chain) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonE2758465DecodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain(l, v) -} -func easyjsonE2758465DecodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain1(in *jlexer.Lexer, out *Rule) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - if in.IsNull() { - in.Skip() - in.WantComma() - continue - } - switch key { - case "Status": - (out.Status).UnmarshalEasyJSON(in) - case "Actions": - easyjsonE2758465DecodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain2(in, &out.Actions) - case "Resources": - easyjsonE2758465DecodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain3(in, &out.Resources) - case "Any": - out.Any = bool(in.Bool()) - case "Condition": - if in.IsNull() { - in.Skip() - out.Condition = nil - } else { - in.Delim('[') - if out.Condition == nil { - if !in.IsDelim(']') { - out.Condition = make([]Condition, 0, 1) - } else { - out.Condition = []Condition{} - } - } else { - out.Condition = (out.Condition)[:0] - } - for !in.IsDelim(']') { - var v7 Condition - easyjsonE2758465DecodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain4(in, &v7) - out.Condition = append(out.Condition, v7) - in.WantComma() - } - in.Delim(']') - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonE2758465EncodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain1(out *jwriter.Writer, in Rule) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"Status\":" - out.RawString(prefix[1:]) - (in.Status).MarshalEasyJSON(out) - } - { - const prefix string = ",\"Actions\":" - out.RawString(prefix) - easyjsonE2758465EncodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain2(out, in.Actions) - } - { - const prefix string = ",\"Resources\":" - out.RawString(prefix) - easyjsonE2758465EncodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain3(out, in.Resources) - } - { - const prefix string = ",\"Any\":" - out.RawString(prefix) - out.Bool(bool(in.Any)) - } - { - const prefix string = ",\"Condition\":" - out.RawString(prefix) - if in.Condition == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { - out.RawString("null") - } else { - out.RawByte('[') - for v8, v9 := range in.Condition { - if v8 > 0 { - out.RawByte(',') - } - easyjsonE2758465EncodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain4(out, v9) - } - out.RawByte(']') - } - } - out.RawByte('}') -} -func easyjsonE2758465DecodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain4(in *jlexer.Lexer, out *Condition) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - if in.IsNull() { - in.Skip() - in.WantComma() - continue - } - switch key { - case "Op": - (out.Op).UnmarshalEasyJSON(in) - case "Kind": - (out.Kind).UnmarshalEasyJSON(in) - case "Key": - out.Key = string(in.String()) - case "Value": - out.Value = string(in.String()) - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonE2758465EncodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain4(out *jwriter.Writer, in Condition) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"Op\":" - out.RawString(prefix[1:]) - (in.Op).MarshalEasyJSON(out) - } - { - const prefix string = ",\"Kind\":" - out.RawString(prefix) - (in.Kind).MarshalEasyJSON(out) - } - { - const prefix string = ",\"Key\":" - out.RawString(prefix) - out.String(string(in.Key)) - } - { - const prefix string = ",\"Value\":" - out.RawString(prefix) - out.String(string(in.Value)) - } - out.RawByte('}') -} -func easyjsonE2758465DecodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain3(in *jlexer.Lexer, out *Resources) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - if in.IsNull() { - in.Skip() - in.WantComma() - continue - } - switch key { - case "Inverted": - out.Inverted = bool(in.Bool()) - case "Names": - if in.IsNull() { - in.Skip() - out.Names = nil - } else { - in.Delim('[') - if out.Names == nil { - if !in.IsDelim(']') { - out.Names = make([]string, 0, 4) - } else { - out.Names = []string{} - } - } else { - out.Names = (out.Names)[:0] - } - for !in.IsDelim(']') { - var v10 string - v10 = string(in.String()) - out.Names = append(out.Names, v10) - in.WantComma() - } - in.Delim(']') - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonE2758465EncodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain3(out *jwriter.Writer, in Resources) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"Inverted\":" - out.RawString(prefix[1:]) - out.Bool(bool(in.Inverted)) - } - { - const prefix string = ",\"Names\":" - out.RawString(prefix) - if in.Names == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { - out.RawString("null") - } else { - out.RawByte('[') - for v11, v12 := range in.Names { - if v11 > 0 { - out.RawByte(',') - } - out.String(string(v12)) - } - out.RawByte(']') - } - } - out.RawByte('}') -} -func easyjsonE2758465DecodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain2(in *jlexer.Lexer, out *Actions) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - if in.IsNull() { - in.Skip() - in.WantComma() - continue - } - switch key { - case "Inverted": - out.Inverted = bool(in.Bool()) - case "Names": - if in.IsNull() { - in.Skip() - out.Names = nil - } else { - in.Delim('[') - if out.Names == nil { - if !in.IsDelim(']') { - out.Names = make([]string, 0, 4) - } else { - out.Names = []string{} - } - } else { - out.Names = (out.Names)[:0] - } - for !in.IsDelim(']') { - var v13 string - v13 = string(in.String()) - out.Names = append(out.Names, v13) - in.WantComma() - } - in.Delim(']') - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonE2758465EncodeGitFrostfsInfoTrueCloudLabPolicyEnginePkgChain2(out *jwriter.Writer, in Actions) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"Inverted\":" - out.RawString(prefix[1:]) - out.Bool(bool(in.Inverted)) - } - { - const prefix string = ",\"Names\":" - out.RawString(prefix) - if in.Names == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { - out.RawString("null") - } else { - out.RawByte('[') - for v14, v15 := range in.Names { - if v14 > 0 { - out.RawByte(',') - } - out.String(string(v15)) - } - out.RawByte(']') - } - } - out.RawByte('}') -} diff --git a/pkg/chain/chain_names.go b/pkg/chain/chain_names.go index 3e9ea5a..8bb88c0 100644 --- a/pkg/chain/chain_names.go +++ b/pkg/chain/chain_names.go @@ -7,7 +7,4 @@ const ( // Ingress represents chains applied when crossing user/storage network boundary. // It is not applied when talking between nodes. Ingress Name = "ingress" - - // S3 represents chains applied when crossing user/s3 network boundary. - S3 Name = "s3" ) diff --git a/pkg/chain/chain_test.go b/pkg/chain/chain_test.go index a5154eb..911daa4 100644 --- a/pkg/chain/chain_test.go +++ b/pkg/chain/chain_test.go @@ -1,30 +1,13 @@ package chain import ( - "fmt" - "strings" "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" - "git.frostfs.info/TrueCloudLab/policy-engine/schema/s3" "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) { expected := Chain{ MatchType: MatchTypeFirstMatch, @@ -52,7 +35,7 @@ func TestEncodeDecode(t *testing.T) { require.Equal(t, expected, actual) } -func TestChainMatch(t *testing.T) { +func TestReturnFirstMatch(t *testing.T) { ch := Chain{ Rules: []Rule{ { @@ -89,940 +72,4 @@ func TestChainMatch(t *testing.T) { require.True(t, found) require.Equal(t, Allow, st) }) - - t.Run("unknown match", func(t *testing.T) { - ch.MatchType = MatchType(255) - request := testutil.NewRequest(native.MethodGetObject, resource, nil) - require.PanicsWithValue(t, "unknown MatchType 255", func() { - ch.Match(request) - }) - }) - - t.Run("no rule found", func(t *testing.T) { - ch.MatchType = MatchTypeFirstMatch - request := testutil.NewRequest(native.MethodGetObject, resource, nil) - st, found := ch.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) -} - -func TestAnyAllConditionMatch(t *testing.T) { - ch := Chain{ - Rules: []Rule{ - { - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{ - { - Op: CondIPAddress, - Kind: KindRequest, - Key: common.PropertyKeyFrostFSSourceIP, - Value: "192.92.1.1/20", - }, - { - Op: CondStringEquals, - Kind: KindRequest, - Key: native.PropertyKeyActorRole, - Value: "owner", - }, - { - Op: CondStringEquals, - Kind: KindResource, - Key: native.PropertyKeyObjectID, - Value: "79xGoKYwhyJQhrDNb7bHhY1WCvN6trHJPTjKkw24c6W9", - }, - }, - }, - }} - - t.Run("match by all conditions", func(t *testing.T) { - ch.Rules[0].Any = false - - resource := testutil.NewResource(native.ResourceFormatRootContainers, map[string]string{ - native.PropertyKeyObjectID: "79xGoKYwhyJQhrDNb7bHhY1WCvN6trHJPTjKkw24c6W9", - }) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.92.1.91", - native.PropertyKeyActorRole: "owner", - }) - - st, found := ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.93.1.91", - }) - - st, found = ch.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) - - t.Run("match by any condition", func(t *testing.T) { - ch.Rules[0].Any = true - - resource := testutil.NewResource(native.ResourceFormatRootContainers, map[string]string{ - native.PropertyKeyObjectID: "79xGoKYwhyJQhrDNb7bHhY1WCvN6trHJPTjKkw24c6W9", - }) - request := testutil.NewRequest(native.MethodPutObject, resource, nil) - - st, found := ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - native.PropertyKeyActorRole: "owner", - }) - - st, found = ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - resource = testutil.NewResource(native.ResourceFormatRootContainers, map[string]string{}) - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.92.1.91", - }) - - st, found = ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.93.1.91", - }) - - st, found = ch.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) -} - -func TestConditionMatch(t *testing.T) { - t.Run("condition types", func(t *testing.T) { - t.Run("slice condition type", testCondSliceContainsMatch) - t.Run("numeric condition types", testNumericConditionsMatch) - t.Run("string condition types", testStringConiditionsMatch) - t.Run("ip conidition types", testIPConditionMatch) - t.Run("unknown condition type", func(t *testing.T) { - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: ConditionType(255), - Kind: KindRequest, - Key: common.PropertyKeyFrostFSSourceIP, - Value: "192.92.1.1/20", - }}, - }}} - resource := testutil.NewResource(native.ResourceFormatRootContainers, map[string]string{ - native.PropertyKeyObjectID: "79xGoKYwhyJQhrDNb7bHhY1WCvN6trHJPTjKkw24c6W9", - }) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.92.1.91", - native.PropertyKeyActorRole: "owner", - }) - require.PanicsWithValue(t, "unimplemented: 255", func() { - ch.Match(request) - }) - }) - }) - - t.Run("kind", func(t *testing.T) { - resource := testutil.NewResource(native.ResourceFormatRootContainers, map[string]string{ - native.PropertyKeyObjectID: "79xGoKYwhyJQhrDNb7bHhY1WCvN6trHJPTjKkw24c6W9", - }) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.92.1.91", - native.PropertyKeyActorRole: "owner", - }) - - t.Run("resource", func(t *testing.T) { - cond := Condition{ - Op: CondStringEquals, - Kind: KindResource, - Key: native.PropertyKeyObjectID, - Value: "79xGoKYwhyJQhrDNb7bHhY1WCvN6trHJPTjKkw24c6W9", - } - - found := cond.Match(request) - require.True(t, found) - }) - t.Run("request", func(t *testing.T) { - cond := Condition{ - Op: CondStringEquals, - Kind: KindRequest, - Key: native.PropertyKeyActorRole, - Value: "owner", - } - - found := cond.Match(request) - require.True(t, found) - }) - t.Run("unknown", func(t *testing.T) { - cond := Condition{ - Op: CondStringEquals, - Kind: ConditionKindType(255), - Key: native.PropertyKeyActorRole, - Value: "owner", - } - - require.PanicsWithValue(t, "unknown condition type: 255", func() { - cond.Match(request) - }) - }) - }) -} - -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, - Kind: KindRequest, - 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()) - }) - } -} - -func testNumericConditionsMatch(t *testing.T) { - propKey := s3.PropertyKeyMaxKeys - - for _, tc := range []struct { - name string - conditions []Condition - value string - status Status - }{ - { - name: "value from interval", - conditions: []Condition{ - { - Op: CondNumericLessThan, - Kind: KindRequest, - Key: propKey, - Value: "100", - }, - { - Op: CondNumericGreaterThan, - Kind: KindRequest, - Key: propKey, - Value: "80", - }, - { - Op: CondNumericNotEquals, - Kind: KindRequest, - Key: propKey, - Value: "91", - }, - }, - value: "90", - status: Allow, - }, - { - name: "border value", - conditions: []Condition{ - { - Op: CondNumericEquals, - Kind: KindRequest, - Key: propKey, - Value: "50", - }, - { - Op: CondNumericLessThanEquals, - Kind: KindRequest, - Key: propKey, - Value: "50", - }, - { - Op: CondNumericGreaterThanEquals, - Kind: KindRequest, - Key: propKey, - Value: "50", - }, - }, - value: "50", - status: Allow, - }, - } { - 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}) - - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: tc.conditions, - }}} - st, _ := ch.Match(request) - require.Equal(t, tc.status.String(), st.String()) - }) - } -} - -func testStringConiditionsMatch(t *testing.T) { - propKey := fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, "some-tag") - val := "tag-value" - - t.Run(CondStringEquals.String(), func(t *testing.T) { - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: CondStringEquals, - Kind: KindRequest, - Key: propKey, - Value: val, - }}, - }}} - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: val, - }) - - st, found := ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: "distort_tag_value" + val, - }) - - st, found = ch.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) - - t.Run(CondStringNotEquals.String(), func(t *testing.T) { - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: CondStringNotEquals, - Kind: KindRequest, - Key: propKey, - Value: val, - }}, - }}} - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: "distort_tag_value" + val, - }) - - st, found := ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: val, - }) - - st, found = ch.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) - - t.Run(CondStringEqualsIgnoreCase.String(), func(t *testing.T) { - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: CondStringEqualsIgnoreCase, - Kind: KindRequest, - Key: propKey, - Value: val, - }}, - }}} - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: strings.ToUpper(val), - }) - - st, found := ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: strings.ToUpper("distort_tag_value" + val), - }) - - st, found = ch.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) - - t.Run(CondStringNotEqualsIgnoreCase.String(), func(t *testing.T) { - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: CondStringNotEqualsIgnoreCase, - Kind: KindRequest, - Key: propKey, - Value: val, - }}, - }}} - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: strings.ToUpper("distort_tag_value" + val), - }) - - st, found := ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: strings.ToUpper(val), - }) - - st, found = ch.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) - - t.Run(CondStringLike.String(), func(t *testing.T) { - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: CondStringLike, - Kind: KindRequest, - Key: propKey, - Value: val + "*", - }}, - }}} - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: val + "suffix", - }) - - st, found := ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: string([]byte(val)[:len(val)-1]), //cut last letter - }) - - st, found = ch.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) - - t.Run(CondStringNotLike.String(), func(t *testing.T) { - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: CondStringNotLike, - Kind: KindRequest, - Key: propKey, - Value: "prefix" + val + "*", - }}, - }}} - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: val + "suffix", - }) - - st, found := ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: "prefix" + val, - }) - - st, found = ch.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) - - t.Run(CondStringLessThan.String(), func(t *testing.T) { - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: CondStringLessThan, - Kind: KindRequest, - Key: propKey, - Value: val + "b", - }}, - }}} - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: val + "a", - }) - - st, found := ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: val + "c", - }) - - st, found = ch.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) - - t.Run(CondStringLessThanEquals.String(), func(t *testing.T) { - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: CondStringLessThanEquals, - Kind: KindRequest, - Key: propKey, - Value: val + "b", - }}, - }}} - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: val + "a", - }) - - st, found := ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: val + "b", - }) - - st, found = ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - }) - - t.Run(CondStringGreaterThan.String(), func(t *testing.T) { - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: CondStringGreaterThan, - Kind: KindRequest, - Key: propKey, - Value: val + "b", - }}, - }}} - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: val + "c", - }) - - st, found := ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: val + "b", - }) - - st, found = ch.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) - - t.Run(CondStringGreaterThanEquals.String(), func(t *testing.T) { - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: CondStringGreaterThanEquals, - Kind: KindRequest, - Key: propKey, - Value: val + "b", - }}, - }}} - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: val + "c", - }) - - st, found := ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - propKey: val + "b", - }) - - st, found = ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - }) -} - -func testIPConditionMatch(t *testing.T) { - t.Run(CondIPAddress.String(), func(t *testing.T) { - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: CondIPAddress, - Kind: KindRequest, - Key: common.PropertyKeyFrostFSSourceIP, - Value: "192.92.1.1/20", - }}, - }}} - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.92.1.91", - }) - - st, found := ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.93.1.91", - }) - - st, found = ch.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) - - t.Run(CondNotIPAddress.String(), func(t *testing.T) { - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: CondNotIPAddress, - Kind: KindRequest, - Key: common.PropertyKeyFrostFSSourceIP, - Value: "192.92.1.1/20", - }}, - }}} - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.93.1.91", - }) - - st, found := ch.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - - request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.92.1.91", - }) - - st, found = ch.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) - - t.Run("invalid ip address condition value", func(t *testing.T) { - ch := Chain{Rules: []Rule{{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: CondIPAddress, - Kind: KindRequest, - Key: common.PropertyKeyFrostFSSourceIP, - Value: "192.92.1.1:33333", - }}, - }}} - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.92.1.91", - }) - - st, found := ch.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) - - t.Run("match ip", func(t *testing.T) { - cond := Condition{ - Op: CondIPAddress, - Kind: KindRequest, - Key: common.PropertyKeyFrostFSSourceIP, - Value: "192.92.1.1/10", - } - - require.NotPanics(t, func() { - cond.matchIP("192.92.1.91") - }) - - require.PanicsWithValue(t, "unimplemented: 10", func() { - cond.Op = CondNumericEquals - cond.matchIP("192.92.1.91") - }) - }) -} - -func TestInvalidNumericValues(t *testing.T) { - t.Run("invalid request property", func(t *testing.T) { - propKey := s3.PropertyKeyMaxKeys - propValues := []string{"", "invalid"} - - for _, tc := range []struct { - name string - conditionType ConditionType - match bool - }{ - { - name: "NumericEquals condition", - conditionType: CondNumericEquals, - match: false, - }, - { - name: "NumericNotEquals condition", - conditionType: CondNumericNotEquals, - match: true, - }, - { - name: "NumericLessThan condition", - conditionType: CondNumericLessThan, - match: false, - }, - { - name: "NumericLessThanEquals condition", - conditionType: CondNumericLessThanEquals, - match: false, - }, - { - name: "NumericGreaterThan condition", - conditionType: CondNumericGreaterThan, - match: false, - }, - { - name: "NumericGreaterThanEquals condition", - conditionType: CondNumericGreaterThanEquals, - match: false, - }, - } { - t.Run(tc.name, func(t *testing.T) { - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - condition := Condition{ - Op: tc.conditionType, - Kind: KindRequest, - Key: propKey, - Value: "50", - } - - for _, propValue := range propValues { - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{propKey: propValue}) - - match := condition.Match(request) - require.Equal(t, tc.match, match) - } - }) - } - }) - t.Run("invalid condition numeric value", func(t *testing.T) { - propKey := s3.PropertyKeyMaxKeys - - condition := Condition{ - Kind: KindRequest, - Key: propKey, - Value: "invalid", - } - - t.Run("match on CondNumericNotEquals", func(t *testing.T) { - condition.Op = CondNumericNotEquals - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{propKey: "50"}) - - match := condition.Match(request) - require.Equal(t, true, match) - }) - - t.Run("non-match on non-CondNumericNotEquals", func(t *testing.T) { - condition.Op = CondNumericEquals - - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{propKey: "50"}) - - match := condition.Match(request) - require.Equal(t, false, match) - }) - - t.Run("match numeric", func(t *testing.T) { - cond := Condition{ - Op: CondNumericLessThan, - Kind: KindRequest, - Key: s3.PropertyKeyMaxKeys, - Value: "5", - } - - require.NotPanics(t, func() { - cond.matchNumeric("6") - }) - - require.PanicsWithValue(t, "unimplemented: 0", func() { - cond.Op = CondStringEquals - cond.matchNumeric("10") - }) - }) - }) -} - -func TestCondTypeStringification(t *testing.T) { - for _, pair := range []struct { - cond ConditionType - expectedString 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"}, - {CondIPAddress, "IPAddress"}, - {CondNotIPAddress, "NotIPAddress"}, - {ConditionType(255), "unknown condition type"}, - } { - require.Equal(t, pair.expectedString, pair.cond.String()) - } -} - -func TestRuleMatch(t *testing.T) { - rule := Rule{ - Status: Allow, - Actions: Actions{Names: []string{native.MethodPutObject}}, - Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, - Condition: []Condition{{ - Op: CondIPAddress, - Kind: KindRequest, - Key: common.PropertyKeyFrostFSSourceIP, - Value: "192.92.1.1/20", - }}, - } - - t.Run("match", func(t *testing.T) { - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.92.1.91", - }) - st, found := rule.Match(request) - require.True(t, found) - require.Equal(t, Allow, st) - }) - - t.Run("not matching resource name", func(t *testing.T) { - resource := testutil.NewResource(fmt.Sprintf(native.ResourceFormatNamespaceContainers, "namespicy"), nil) - request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.92.1.91", - }) - st, found := rule.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) - - t.Run("not matching action", func(t *testing.T) { - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodGetObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.92.1.91", - }) - st, found := rule.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) - - t.Run("not matching condition", func(t *testing.T) { - resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) - request := testutil.NewRequest(native.MethodGetObject, resource, map[string]string{ - common.PropertyKeyFrostFSSourceIP: "192.93.1.91", - }) - st, found := rule.Match(request) - require.False(t, found) - require.Equal(t, NoRuleFound, st) - }) } diff --git a/pkg/chain/marshal_binary.go b/pkg/chain/marshal_binary.go deleted file mode 100644 index 357cc79..0000000 --- a/pkg/chain/marshal_binary.go +++ /dev/null @@ -1,257 +0,0 @@ -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.Kind)) - 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.Kind = ConditionKindType(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 -} diff --git a/pkg/chain/marshal_binary_test.go b/pkg/chain/marshal_binary_test.go deleted file mode 100644 index 4cfd0cb..0000000 --- a/pkg/chain/marshal_binary_test.go +++ /dev/null @@ -1,247 +0,0 @@ -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 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, - Kind: ot, - Key: "", - Value: "", - }) - - result[2] = append(result[2], Condition{ - Op: ct, - Kind: ot, - Key: "key", - Value: "", - }) - - result[2] = append(result[2], Condition{ - Op: ct, - Kind: ot, - Key: "", - Value: "value", - }) - - result[2] = append(result[2], Condition{ - Op: ct, - Kind: 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() []ConditionKindType { - return []ConditionKindType{ - KindResource, - KindRequest, - } -} - -func generateTestMatchTypes() []MatchType { - return []MatchType{ - MatchTypeDenyPriority, - MatchTypeFirstMatch, - } -} diff --git a/pkg/chain/marshal_fuzz.go b/pkg/chain/marshal_fuzz.go deleted file mode 100644 index b5cbec9..0000000 --- a/pkg/chain/marshal_fuzz.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build gofuzz -// +build gofuzz - -package chain - -func DoFuzzChainUnmarshalBinary(data []byte) int { - var ch Chain - err := ch.UnmarshalBinary(data) - if err != nil { - return 0 - } - return 1 -} diff --git a/pkg/chain/marshal_fuzz_test.go b/pkg/chain/marshal_fuzz_test.go deleted file mode 100644 index f6956c5..0000000 --- a/pkg/chain/marshal_fuzz_test.go +++ /dev/null @@ -1,34 +0,0 @@ -//go:build gofuzz -// +build gofuzz - -package chain - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -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) { - require.NotPanics(t, func() { - DoFuzzChainUnmarshalBinary(data) - }) - }) -} diff --git a/pkg/chain/marshal_json.go b/pkg/chain/marshal_json.go deleted file mode 100644 index a83c816..0000000 --- a/pkg/chain/marshal_json.go +++ /dev/null @@ -1,145 +0,0 @@ -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 ConditionKindType - str string -}{ - {KindRequest, "Request"}, - {KindResource, "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 ConditionKindType) 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 *ConditionKindType) 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 = ConditionKindType(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) -} diff --git a/pkg/chain/marshal_json_test.go b/pkg/chain/marshal_json_test.go deleted file mode 100644 index c4bc621..0000000 --- a/pkg/chain/marshal_json_test.go +++ /dev/null @@ -1,121 +0,0 @@ -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, - Kind: KindRequest, - 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, - Kind: KindResource, - Key: native.PropertyKeyObjectType, - Value: "regular", - }, - }, - }, - { - Status: Status(100), - Condition: []Condition{ - { - Op: ConditionType(255), - Kind: ConditionKindType(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) -} diff --git a/pkg/chain/testdata/test_status_json.json b/pkg/chain/testdata/test_status_json.json deleted file mode 100644 index 6955ce7..0000000 --- a/pkg/chain/testdata/test_status_json.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "ID": "MmNjYTVhZTctY2VlOC00MjhkLWI0NWYtNTY3ZmIxZDAzZjAx", - "Rules": [ - { - "Status": "AccessDenied", - "Actions": { - "Inverted": false, - "Names": [ - "DeleteObject", - "GetContainer" - ] - }, - "Resources": { - "Inverted": false, - "Names": [ - "native:object/*" - ] - }, - "Any": false, - "Condition": [ - { - "Op": "StringEquals", - "Kind": "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", - "Kind": "Resource", - "Key": "$Object:objectType", - "Value": "regular" - } - ] - }, - { - "Status": "100", - "Actions": { - "Inverted": false, - "Names": null - }, - "Resources": { - "Inverted": false, - "Names": null - }, - "Any": false, - "Condition": [ - { - "Op": "255", - "Kind": "128", - "Key": "", - "Value": "" - } - ] - } - ], - "MatchType": "FirstMatch" -} diff --git a/pkg/engine/chain_router.go b/pkg/engine/chain_router.go index 3eced4a..93775b8 100644 --- a/pkg/engine/chain_router.go +++ b/pkg/engine/chain_router.go @@ -6,18 +6,18 @@ import ( ) type defaultChainRouter struct { - morph MorphRuleChainStorageReader + morph MorphRuleChainStorage local LocalOverrideStorage } -func NewDefaultChainRouter(morph MorphRuleChainStorageReader) ChainRouter { +func NewDefaultChainRouter(morph MorphRuleChainStorage) ChainRouter { return &defaultChainRouter{ morph: morph, } } -func NewDefaultChainRouterWithLocalOverrides(morph MorphRuleChainStorageReader, local LocalOverrideStorage) ChainRouter { +func NewDefaultChainRouterWithLocalOverrides(morph MorphRuleChainStorage, local LocalOverrideStorage) ChainRouter { return &defaultChainRouter{ morph: morph, local: local, @@ -42,36 +42,43 @@ func (dr *defaultChainRouter) checkLocal(name chain.Name, rt RequestTarget, r re if dr.local == nil { return } - - var hasAllow bool + var ruleFounds []bool for _, target := range rt.Targets() { status, ruleFound, err = dr.matchLocalOverrides(name, target, r) if err != nil || ruleFound && status != chain.Allow { return } - hasAllow = hasAllow || ruleFound + ruleFounds = append(ruleFounds, ruleFound) } - if hasAllow { - return chain.Allow, true, nil + status = chain.NoRuleFound + for _, ruleFound = range ruleFounds { + if ruleFound { + status = chain.Allow + break + } } - return chain.NoRuleFound, false, nil + return } func (dr *defaultChainRouter) checkMorph(name chain.Name, rt RequestTarget, r resource.Request) (status chain.Status, ruleFound bool, err error) { - var hasAllow bool + var ruleFounds []bool for _, target := range rt.Targets() { status, ruleFound, err = dr.matchMorphRuleChains(name, target, r) if err != nil || ruleFound && status != chain.Allow { return } - hasAllow = hasAllow || ruleFound + ruleFounds = append(ruleFounds, ruleFound) } - if hasAllow { - return chain.Allow, true, nil + status = chain.NoRuleFound + for _, ruleFound = range ruleFounds { + if ruleFound { + status = chain.Allow + break + } } - return chain.NoRuleFound, false, nil + return } func (dr *defaultChainRouter) matchLocalOverrides(name chain.Name, target Target, r resource.Request) (status chain.Status, ruleFound bool, err error) { diff --git a/pkg/engine/inmemory/inmemory_test.go b/pkg/engine/inmemory/inmemory_test.go index 30bd7c3..1706c2a 100644 --- a/pkg/engine/inmemory/inmemory_test.go +++ b/pkg/engine/inmemory/inmemory_test.go @@ -1,79 +1,14 @@ package inmemory import ( - "bytes" "testing" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" resourcetest "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil" - "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) -func TestAddRootOverrides(t *testing.T) { - s := NewInMemoryLocalOverrides() - - target := engine.NamespaceTarget("") - - id, err := s.LocalStorage().AddOverride(chain.S3, target, &chain.Chain{ - Rules: []chain.Rule{{ - Status: chain.Allow, - Actions: chain.Actions{Names: []string{"s3:PutObject"}}, - Resources: chain.Resources{Names: []string{"*"}}, - }}, - }) - require.NoError(t, err) - - res, err := s.LocalStorage().ListOverrides(chain.S3, target) - require.NoError(t, err) - require.Len(t, res, 1) - require.Equal(t, string(id), string(res[0].ID)) -} - -func TestInmemory_MultipleTargets(t *testing.T) { - const op = "ape::test::op" - - targets := []engine.Target{ - engine.NamespaceTarget("ns1"), - engine.ContainerTarget("cnr1"), - engine.GroupTarget("group1"), - engine.UserTarget("user1"), - } - req := resourcetest.NewRequest(op, resourcetest.NewResource("r", nil), nil) - target := engine.NewRequestTargetExtended("ns1", "cnr1", "user1", []string{"group1"}) - for _, tt := range targets { - t.Run("morph", func(t *testing.T) { - s := NewInMemoryLocalOverrides() - s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, tt, &chain.Chain{ - Rules: []chain.Rule{{ - Status: chain.Allow, - Actions: chain.Actions{Names: []string{op}}, - }}, - }) - - status, found, err := s.IsAllowed(chain.Ingress, target, req) - require.NoError(t, err) - require.True(t, found) - require.Equal(t, chain.Allow, status) - }) - t.Run("override", func(t *testing.T) { - s := NewInMemoryLocalOverrides() - s.LocalStorage().AddOverride(chain.Ingress, tt, &chain.Chain{ - Rules: []chain.Rule{{ - Status: chain.Allow, - Actions: chain.Actions{Names: []string{op}}, - }}, - }) - - status, found, err := s.IsAllowed(chain.Ingress, target, req) - require.NoError(t, err) - require.True(t, found) - require.Equal(t, chain.Allow, status) - }) - } -} - func TestInmemory(t *testing.T) { const ( object = "native::object::abc/xyz" @@ -112,26 +47,22 @@ func TestInmemory(t *testing.T) { Any: true, Condition: []chain.Condition{ { - Op: chain.CondStringNotLike, - Kind: chain.KindRequest, - Key: "SourceIP", - Value: "10.1.1.*", + Op: chain.CondStringNotLike, + Object: chain.ObjectRequest, + Key: "SourceIP", + Value: "10.1.1.*", }, { - Op: chain.CondStringNotEquals, - Kind: chain.KindRequest, - Key: "Actor", - Value: actor1, + Op: chain.CondStringNotEquals, + Object: chain.ObjectRequest, + Key: "Actor", + Value: actor1, }, }, }, }, }) - _, it, err := s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace) - require.NoError(t, err) - itemStacksEqual(t, it.Values, toStackItems(namespace)) - s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace2), &chain.Chain{ Rules: []chain.Rule{ { // Deny all expect "native::object::get" for all objects expect "native::object::abc/xyz". @@ -142,10 +73,6 @@ func TestInmemory(t *testing.T) { }, }) - _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace) - require.NoError(t, err) - itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2)) - s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(container), &chain.Chain{ Rules: []chain.Rule{ { // Allow to actor2 to get objects from the specific container only if they have `Department=HR` attribute. @@ -154,30 +81,22 @@ func TestInmemory(t *testing.T) { Resources: chain.Resources{Names: []string{"native::object::abc/*"}}, Condition: []chain.Condition{ { - Op: chain.CondStringEquals, - Kind: chain.KindResource, - Key: "Department", - Value: "HR", + Op: chain.CondStringEquals, + Object: chain.ObjectResource, + Key: "Department", + Value: "HR", }, { - Op: chain.CondStringEquals, - Kind: chain.KindRequest, - Key: "Actor", - Value: actor2, + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: "Actor", + Value: actor2, }, }, }, }, }) - _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace) - require.NoError(t, err) - itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2)) - - _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Container) - require.NoError(t, err) - itemStacksEqual(t, it.Values, toStackItems(container)) - t.Run("bad subnet, namespace deny", func(t *testing.T) { // Request initiating from the untrusted subnet. reqBadIP := resourcetest.NewRequest("native::object::put", res, map[string]string{ @@ -256,14 +175,6 @@ func TestInmemory(t *testing.T) { }}, }) - _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace) - require.NoError(t, err) - itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2)) - - _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Container) - require.NoError(t, err) - itemStacksEqual(t, it.Values, toStackItems(container)) - status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqGood) require.Equal(t, chain.NoRuleFound, status) require.False(t, ok) @@ -279,14 +190,6 @@ func TestInmemory(t *testing.T) { }}, }) - _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace) - require.NoError(t, err) - itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2)) - - _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Container) - require.NoError(t, err) - itemStacksEqual(t, it.Values, toStackItems(container)) - status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqGood) require.Equal(t, chain.QuotaLimitReached, status) require.True(t, ok) @@ -295,83 +198,9 @@ func TestInmemory(t *testing.T) { err := s.LocalStorage().RemoveOverride(chain.Ingress, engine.ContainerTarget(container), quotaRuleChainID) require.NoError(t, err) - _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace) - require.NoError(t, err) - itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2)) - - _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Container) - require.NoError(t, err) - itemStacksEqual(t, it.Values, toStackItems(container)) - status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqGood) require.Equal(t, chain.NoRuleFound, status) require.False(t, ok) }) }) - - t.Run("remove all", func(t *testing.T) { - s := NewInMemoryLocalOverrides() - _, _, err := s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace), &chain.Chain{ - Rules: []chain.Rule{ - { - Status: chain.AccessDenied, - Actions: chain.Actions{Inverted: true, Names: []string{"native::object::get"}}, - Resources: chain.Resources{Inverted: true, Names: []string{object}}, - }, - }, - }) - require.NoError(t, err) - _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace2), &chain.Chain{ - Rules: []chain.Rule{ - { - Status: chain.Allow, - Actions: chain.Actions{Inverted: true, Names: []string{"native::object::get"}}, - Resources: chain.Resources{Inverted: true, Names: []string{object}}, - }, - }, - }) - require.NoError(t, err) - _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace2), &chain.Chain{ - Rules: []chain.Rule{ - { - Status: chain.AccessDenied, - Actions: chain.Actions{Inverted: true, Names: []string{"native::object::get"}}, - Resources: chain.Resources{Inverted: true, Names: []string{object}}, - }, - }, - }) - require.NoError(t, err) - _, _, err = s.MorphRuleChainStorage().RemoveMorphRuleChainsByTarget(chain.Ingress, engine.NamespaceTarget(namespace2)) - require.NoError(t, err) - chains, err := s.MorphRuleChainStorage().ListMorphRuleChains(chain.Ingress, engine.NamespaceTarget(namespace2)) - require.NoError(t, err) - require.Equal(t, 0, len(chains)) - chains, err = s.MorphRuleChainStorage().ListMorphRuleChains(chain.Ingress, engine.NamespaceTarget(namespace)) - require.NoError(t, err) - require.Equal(t, 1, len(chains)) - }) -} - -func itemStacksEqual(t *testing.T, got []stackitem.Item, expected []stackitem.Item) { -next: - for _, exp := range expected { - expBytes, err := exp.TryBytes() - require.NoError(t, err) - for _, v := range got { - vBytes, err := v.TryBytes() - require.NoError(t, err) - if bytes.Equal(vBytes, expBytes) { - continue next - } - } - t.Fatalf("not found %s", exp) - } -} - -func toStackItems(names ...string) []stackitem.Item { - var items []stackitem.Item - for _, name := range names { - items = append(items, stackitem.NewByteArray([]byte(name))) - } - return items } diff --git a/pkg/engine/inmemory/local_storage.go b/pkg/engine/inmemory/local_storage.go index b585816..4055657 100644 --- a/pkg/engine/inmemory/local_storage.go +++ b/pkg/engine/inmemory/local_storage.go @@ -1,11 +1,9 @@ package inmemory import ( - "bytes" "fmt" "math/rand" "strings" - "sync" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" @@ -15,16 +13,14 @@ import ( type targetToChain map[engine.Target][]*chain.Chain type inmemoryLocalStorage struct { - usedChainID map[string]struct{} + usedChainID map[chain.ID]struct{} nameToResourceChains map[chain.Name]targetToChain - guard *sync.RWMutex } func NewInmemoryLocalStorage() engine.LocalOverrideStorage { return &inmemoryLocalStorage{ - usedChainID: map[string]struct{}{}, + usedChainID: map[chain.ID]struct{}{}, nameToResourceChains: make(map[chain.Name]targetToChain), - guard: &sync.RWMutex{}, } } @@ -36,28 +32,20 @@ func (s *inmemoryLocalStorage) generateChainID(name chain.Name, target engine.Ta sid = strings.ReplaceAll(sid, "*", "") sid = strings.ReplaceAll(sid, "/", ":") sid = strings.ReplaceAll(sid, "::", ":") - _, ok := s.usedChainID[sid] + id = chain.ID(sid) + _, ok := s.usedChainID[id] if ok { continue } - s.usedChainID[sid] = struct{}{} - - id = chain.ID(sid) + s.usedChainID[id] = struct{}{} break } return id } func (s *inmemoryLocalStorage) AddOverride(name chain.Name, target engine.Target, c *chain.Chain) (chain.ID, error) { - s.guard.Lock() - defer s.guard.Unlock() - - if target.Name == "" { - target.Name = "root" - } - // AddOverride assigns generated chain ID if it has not been assigned. - if len(c.ID) == 0 { + if c.ID == "" { c.ID = s.generateChainID(name, target) } if s.nameToResourceChains[name] == nil { @@ -65,7 +53,7 @@ func (s *inmemoryLocalStorage) AddOverride(name chain.Name, target engine.Target } rc := s.nameToResourceChains[name] for i := range rc[target] { - if bytes.Equal(rc[target][i].ID, c.ID) { + if rc[target][i].ID == c.ID { rc[target][i] = c return c.ID, nil } @@ -75,21 +63,15 @@ 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) { - s.guard.RLock() - defer s.guard.RUnlock() - if _, ok := s.nameToResourceChains[name]; !ok { return nil, engine.ErrChainNameNotFound } - if target.Name == "" { - target.Name = "root" - } chains, ok := s.nameToResourceChains[name][target] if !ok { return nil, engine.ErrResourceNotFound } for _, c := range chains { - if bytes.Equal(c.ID, chainID) { + if c.ID == chainID { return c, nil } } @@ -97,21 +79,15 @@ func (s *inmemoryLocalStorage) GetOverride(name chain.Name, target engine.Target } 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 { return engine.ErrChainNameNotFound } - if target.Name == "" { - target.Name = "root" - } chains, ok := s.nameToResourceChains[name][target] if !ok { return engine.ErrResourceNotFound } for i, c := range chains { - if bytes.Equal(c.ID, chainID) { + if c.ID == chainID { s.nameToResourceChains[name][target] = append(chains[:i], chains[i+1:]...) return nil } @@ -119,35 +95,11 @@ func (s *inmemoryLocalStorage) RemoveOverride(name chain.Name, target engine.Tar return engine.ErrChainNotFound } -func (s *inmemoryLocalStorage) RemoveOverridesByTarget(name chain.Name, target engine.Target) error { - s.guard.Lock() - defer s.guard.Unlock() - - if _, ok := s.nameToResourceChains[name]; !ok { - return engine.ErrChainNameNotFound - } - if target.Name == "" { - target.Name = "root" - } - _, ok := s.nameToResourceChains[name][target] - if ok { - delete(s.nameToResourceChains[name], target) - return nil - } - return engine.ErrResourceNotFound -} - 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] if !ok { return []*chain.Chain{}, nil } - if target.Name == "" { - target.Name = "root" - } for t, chains := range rcs { if t.Type != target.Type { continue @@ -161,20 +113,6 @@ func (s *inmemoryLocalStorage) ListOverrides(name chain.Name, target engine.Targ } func (s *inmemoryLocalStorage) DropAllOverrides(name chain.Name) error { - s.guard.Lock() - defer s.guard.Unlock() - s.nameToResourceChains[name] = make(targetToChain) 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 -} diff --git a/pkg/engine/inmemory/local_storage_test.go b/pkg/engine/inmemory/local_storage_test.go index f21d94d..3609070 100644 --- a/pkg/engine/inmemory/local_storage_test.go +++ b/pkg/engine/inmemory/local_storage_test.go @@ -14,7 +14,9 @@ const ( nonExistChainId = "ingress:LxGyWyL" ) -var resrc = engine.ContainerTarget(container) +var ( + resrc = engine.ContainerTarget(container) +) func testInmemLocalStorage() *inmemoryLocalStorage { return NewInmemoryLocalStorage().(*inmemoryLocalStorage) @@ -110,52 +112,6 @@ func TestRemoveOverride(t *testing.T) { require.True(t, ok) require.Len(t, resourceChains, 0) }) - - t.Run("remove by target", func(t *testing.T) { - inmem := testInmemLocalStorage() - t0 := engine.ContainerTarget("name0") - t1 := engine.ContainerTarget("name1") - inmem.AddOverride(chain.Ingress, t0, &chain.Chain{ - ID: chain.ID(chainID), - Rules: []chain.Rule{ - { - Status: chain.AccessDenied, - Actions: chain.Actions{Names: []string{"native::object::delete"}}, - Resources: chain.Resources{Names: []string{"native::object::*"}}, - }, - }, - }) - inmem.AddOverride(chain.Ingress, t0, &chain.Chain{ - ID: chain.ID(chainID), - Rules: []chain.Rule{ - { - Status: chain.Allow, - Actions: chain.Actions{Names: []string{"native::object::delete"}}, - Resources: chain.Resources{Names: []string{"native::object::*"}}, - }, - }, - }) - inmem.AddOverride(chain.Ingress, t1, &chain.Chain{ - ID: chain.ID(chainID), - Rules: []chain.Rule{ - { - Status: chain.Allow, - Actions: chain.Actions{Names: []string{"native::object::delete"}}, - Resources: chain.Resources{Names: []string{"native::object::*"}}, - }, - }, - }) - - err := inmem.RemoveOverridesByTarget(chain.Ingress, t0) - require.NoError(t, err) - - ingressChains, ok := inmem.nameToResourceChains[chain.Ingress] - require.True(t, ok) - require.Len(t, ingressChains, 1) - resourceChains, ok := ingressChains[t1] - require.True(t, ok) - require.Len(t, resourceChains, 1) - }) } func TestGetOverride(t *testing.T) { @@ -230,9 +186,6 @@ func TestListOverrides(t *testing.T) { inmem.AddOverride(chain.Ingress, resrc, addChain) l, _ := inmem.ListOverrides(chain.Ingress, resrc) 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) { @@ -257,12 +210,12 @@ func TestGenerateID(t *testing.T) { } func hasDuplicates(ids []chain.ID) bool { - seen := make(map[string]bool) + seen := make(map[chain.ID]bool) for _, id := range ids { - if seen[string(id)] { + if seen[id] { return true } - seen[string(id)] = true + seen[id] = true } return false } diff --git a/pkg/engine/inmemory/morph_storage.go b/pkg/engine/inmemory/morph_storage.go index 4aa96e0..72b5439 100644 --- a/pkg/engine/inmemory/morph_storage.go +++ b/pkg/engine/inmemory/morph_storage.go @@ -3,26 +3,27 @@ package inmemory import ( "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" - "github.com/google/uuid" - "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) type inmemoryMorphRuleChainStorage struct { - storage engine.LocalOverrideStorage + nameToNamespaceChains engine.LocalOverrideStorage + nameToContainerChains engine.LocalOverrideStorage } func NewInmemoryMorphRuleChainStorage() engine.MorphRuleChainStorage { return &inmemoryMorphRuleChainStorage{ - storage: NewInmemoryLocalStorage(), + nameToNamespaceChains: NewInmemoryLocalStorage(), + nameToContainerChains: NewInmemoryLocalStorage(), } } func (s *inmemoryMorphRuleChainStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (_ util.Uint256, _ uint32, err error) { switch target.Type { - case engine.Namespace, engine.Container, engine.User, engine.Group: - _, err = s.storage.AddOverride(name, target, c) + case engine.Namespace: + _, err = s.nameToNamespaceChains.AddOverride(name, target, c) + case engine.Container: + _, err = s.nameToContainerChains.AddOverride(name, target, c) default: err = engine.ErrUnknownTarget } @@ -31,18 +32,10 @@ func (s *inmemoryMorphRuleChainStorage) AddMorphRuleChain(name chain.Name, targe func (s *inmemoryMorphRuleChainStorage) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (_ util.Uint256, _ uint32, err error) { switch target.Type { - case engine.Namespace, engine.Container, engine.User, engine.Group: - err = s.storage.RemoveOverride(name, target, chainID) - default: - err = engine.ErrUnknownTarget - } - return -} - -func (s *inmemoryMorphRuleChainStorage) RemoveMorphRuleChainsByTarget(name chain.Name, target engine.Target) (_ util.Uint256, _ uint32, err error) { - switch target.Type { - case engine.Namespace, engine.Container, engine.User, engine.Group: - err = s.storage.RemoveOverridesByTarget(name, target) + case engine.Namespace: + err = s.nameToNamespaceChains.RemoveOverride(name, target, chainID) + case engine.Container: + err = s.nameToContainerChains.RemoveOverride(name, target, chainID) default: err = engine.ErrUnknownTarget } @@ -51,62 +44,11 @@ func (s *inmemoryMorphRuleChainStorage) RemoveMorphRuleChainsByTarget(name chain func (s *inmemoryMorphRuleChainStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) { switch target.Type { - case engine.Namespace, engine.Container, engine.User, engine.Group: - return s.storage.ListOverrides(name, target) + case engine.Namespace: + return s.nameToNamespaceChains.ListOverrides(name, target) + case engine.Container: + return s.nameToContainerChains.ListOverrides(name, target) default: } 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") -} - -func (s *inmemoryMorphRuleChainStorage) ListTargetsIterator(targetType engine.TargetType) (uuid uuid.UUID, it result.Iterator, err error) { - it.Values = make([]stackitem.Item, 0) - - switch targetType { - case engine.Namespace: - // Listing targets may look bizarre, because inmemory rule chain storage use inmemory local overrides where - // targets are listed by chain names. - var targets []engine.Target - targets, err = s.storage.ListOverrideDefinedTargets(chain.Ingress) - if err != nil { - return - } - for _, t := range targets { - it.Values = append(it.Values, stackitem.NewByteArray([]byte(t.Name))) - } - - targets, err = s.storage.ListOverrideDefinedTargets(chain.S3) - if err != nil { - return - } - for _, t := range targets { - it.Values = append(it.Values, stackitem.NewByteArray([]byte(t.Name))) - } - case engine.Container: - var targets []engine.Target - targets, err = s.storage.ListOverrideDefinedTargets(chain.Ingress) - if err != nil { - return - } - for _, t := range targets { - it.Values = append(it.Values, stackitem.NewByteArray([]byte(t.Name))) - } - - targets, err = s.storage.ListOverrideDefinedTargets(chain.S3) - if err != nil { - return - } - for _, t := range targets { - it.Values = append(it.Values, stackitem.NewByteArray([]byte(t.Name))) - } - default: - } - return -} diff --git a/pkg/engine/interface.go b/pkg/engine/interface.go index ab026b3..8d11466 100644 --- a/pkg/engine/interface.go +++ b/pkg/engine/interface.go @@ -3,8 +3,6 @@ package engine import ( "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" - "github.com/google/uuid" - "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/util" ) @@ -23,13 +21,9 @@ type LocalOverrideStorage interface { RemoveOverride(name chain.Name, target Target, chainID chain.ID) error - RemoveOverridesByTarget(name chain.Name, target Target) error - ListOverrides(name chain.Name, target Target) ([]*chain.Chain, error) DropAllOverrides(name chain.Name) error - - ListOverrideDefinedTargets(name chain.Name) ([]Target, error) } type TargetType rune @@ -37,8 +31,6 @@ type TargetType rune const ( Namespace TargetType = 'n' Container TargetType = 'c' - User TargetType = 'u' - Group TargetType = 'g' ) type Target struct { @@ -50,8 +42,6 @@ type Target struct { type RequestTarget struct { Namespace *Target Container *Target - User *Target - Groups []Target } func NewRequestTargetWithNamespace(namespace string) RequestTarget { @@ -77,36 +67,12 @@ func NewRequestTarget(namespace, container string) RequestTarget { } } -func NewRequestTargetExtended(namespace, container, user string, groups []string) RequestTarget { - nt := NamespaceTarget(namespace) - ct := ContainerTarget(container) - u := UserTarget(user) - rt := RequestTarget{ - Namespace: &nt, - Container: &ct, - User: &u, - } - if len(groups) != 0 { - rt.Groups = make([]Target, len(groups)) - for i := range groups { - rt.Groups[i] = GroupTarget(groups[i]) - } - } - return rt -} - func (rt *RequestTarget) Targets() (targets []Target) { - if rt.Namespace != nil { - targets = append(targets, *rt.Namespace) - } if rt.Container != nil { targets = append(targets, *rt.Container) } - if rt.User != nil { - targets = append(targets, *rt.User) - } - if len(rt.Groups) != 0 { - targets = append(targets, rt.Groups...) + if rt.Namespace != nil { + targets = append(targets, *rt.Namespace) } return } @@ -125,47 +91,17 @@ func ContainerTarget(container string) Target { } } -func UserTarget(user string) Target { - return Target{ - Type: User, - Name: user, - } -} - -func GroupTarget(group string) Target { - return Target{ - Type: Group, - Name: group, - } -} - -// 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) - - // ListTargetsIterator provides an iterator to list targets for which rules are defined. - ListTargetsIterator(targetType TargetType) (uuid.UUID, result.Iterator, error) -} - -// MorphRuleChainStorage is the interface to read and manage data within a chain storage. +// MorphRuleChainStorage is the interface to manage chains from the chain storage. // Basically, this implies that the storage manages rules stored in policy contract. type MorphRuleChainStorage interface { - MorphRuleChainStorageReader - // 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) // 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) - // RemoveMorphRuleChainsByTarget removes all chains by target and returns transaction hash, VUB and error. - RemoveMorphRuleChainsByTarget(name chain.Name, target Target) (util.Uint256, uint32, error) - - SetAdmin(addr util.Uint160) (util.Uint256, uint32, error) + // ListMorphRuleChains just lists deserialized chains. + ListMorphRuleChains(name chain.Name, target Target) ([]*chain.Chain, error) } // Engine is the interface that provides methods to check request permissions checking diff --git a/pkg/marshal/marshal.go b/pkg/marshal/marshal.go deleted file mode 100644 index e296de6..0000000 --- a/pkg/marshal/marshal.go +++ /dev/null @@ -1,267 +0,0 @@ -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, - } -} diff --git a/pkg/marshal/marshal_test.go b/pkg/marshal/marshal_test.go deleted file mode 100644 index 5d3babd..0000000 --- a/pkg/marshal/marshal_test.go +++ /dev/null @@ -1,313 +0,0 @@ -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) - }) - }) -} diff --git a/pkg/morph/policy/policy_contract_storage.go b/pkg/morph/policy/policy_contract_storage.go index ed5a062..6e85339 100644 --- a/pkg/morph/policy/policy_contract_storage.go +++ b/pkg/morph/policy/policy_contract_storage.go @@ -6,15 +6,11 @@ import ( "math/big" "strings" - "git.frostfs.info/TrueCloudLab/frostfs-contract/commonclient" "git.frostfs.info/TrueCloudLab/frostfs-contract/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/engine" - "github.com/google/uuid" - "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" - neoinvoker "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" @@ -26,66 +22,29 @@ var ( ErrEngineTargetTypeUnsupported = errors.New("this target type is not supported yet") ) -// ContractStorage is the interface to manage chain rules within Policy contract. +// ContractStorage is the interface to manage chain rules within the policy contract. type ContractStorage struct { - hash util.Uint160 - - actor ContractStorageActor - contractInterface *client.Contract } var _ engine.MorphRuleChainStorage = (*ContractStorage)(nil) -// ContractStorageReader is the interface to read data from Policy contract. -type ContractStorageReader struct { - hash util.Uint160 - - invoker ContractStorageInvoker - - contractReaderInterface *client.ContractReader -} - -type ContractStorageActor interface { - client.Actor - GetRPCInvoker() neoinvoker.RPCInvoke -} - -var _ engine.MorphRuleChainStorageReader = (*ContractStorageReader)(nil) - -func NewContractStorage(actor ContractStorageActor, contract util.Uint160) *ContractStorage { +func NewContractStorage(actor client.Actor, contract util.Uint160) *ContractStorage { return &ContractStorage{ - hash: contract, - actor: actor, contractInterface: client.New(actor, contract), } } -type contractStorageActorImpl struct { - client.Actor - rpcActor actor.RPCActor -} - -var _ ContractStorageActor = &contractStorageActorImpl{} - -func (c *contractStorageActorImpl) GetRPCInvoker() neoinvoker.RPCInvoke { - return c.rpcActor -} - -// NewContractStorageWithSimpleActor constructs core actor from `rpcActor`. -// -// Note: NewContractStorageWithSimpleActor is appropriate only for call-only-once cases (for example, in CLIs). Otherwise, it is unsafe, -// because core actor may use invalidated `rpcActor` if some connection errors occurred. func NewContractStorageWithSimpleActor(rpcActor actor.RPCActor, acc *wallet.Account, contract util.Uint160) (*ContractStorage, error) { act, err := actor.NewSimple(rpcActor, acc) if err != nil { return nil, fmt.Errorf("failed to create simple actor: %w", err) } - return NewContractStorage(&contractStorageActorImpl{Actor: act, rpcActor: rpcActor}, contract), nil + return NewContractStorage(act, contract), nil } func (s *ContractStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (txHash util.Uint256, vub uint32, err error) { - if len(c.ID) == 0 { + if c.ID == "" { err = ErrEmptyChainID return } @@ -102,7 +61,7 @@ func (s *ContractStorage) AddMorphRuleChain(name chain.Name, target engine.Targe } func (s *ContractStorage) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (txHash util.Uint256, vub uint32, err error) { - if len(chainID) == 0 { + if chainID == "" { err = ErrEmptyChainID return } @@ -118,38 +77,17 @@ func (s *ContractStorage) RemoveMorphRuleChain(name chain.Name, target engine.Ta return } -func (s *ContractStorage) RemoveMorphRuleChainsByTarget(name chain.Name, target engine.Target) (txHash util.Uint256, vub uint32, err error) { - var kind policy.Kind - kind, err = policyKind(target.Type) - if err != nil { - return - } - fullName := prefixedChainName(name, nil) - - txHash, vub, err = s.contractInterface.RemoveChainsByPrefix(big.NewInt(int64(kind)), target.Name, fullName) - return -} - -func listChains(name chain.Name, target engine.Target, rpcInvoker neoinvoker.RPCInvoke, hash util.Uint160) ([]*chain.Chain, error) { +func (s *ContractStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) { kind, err := policyKind(target.Type) if err != nil { return nil, err } - const ( - method = "iteratorChainsByPrefix" - batchSize = neoinvoker.DefaultIteratorResultItems - ) - - inv := neoinvoker.New(rpcInvoker, nil) - params := []any{ - big.NewInt(int64(kind)), target.Name, []byte(name), - } - - items, err := commonclient.ReadIteratorItems(inv, batchSize, hash, method, params...) + items, err := s.contractInterface.ListChainsByPrefix(big.NewInt(int64(kind)), target.Name, []byte(name)) if err != nil { - return nil, fmt.Errorf("read items error: %w", err) + return nil, err } + var chains []*chain.Chain for _, item := range items { serialized, err := bytesFromStackItem(item) @@ -162,58 +100,10 @@ func listChains(name chain.Name, target engine.Target, rpcInvoker neoinvoker.RPC } chains = append(chains, c) } + return chains, nil } -func (s *ContractStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) { - return listChains(name, target, s.actor.GetRPCInvoker(), s.hash) -} - -func (s *ContractStorage) ListTargetsIterator(targetType engine.TargetType) (uuid.UUID, result.Iterator, error) { - kind, err := policyKind(targetType) - if err != nil { - return uuid.UUID{}, result.Iterator{}, err - } - return s.contractInterface.ListTargets(big.NewInt(int64(kind))) -} - -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) -} - -type ContractStorageInvoker interface { - client.Invoker - GetRPCInvoker() neoinvoker.RPCInvoke -} - -func NewContractStorageReader(inv ContractStorageInvoker, contract util.Uint160) *ContractStorageReader { - return &ContractStorageReader{ - hash: contract, - invoker: inv, - contractReaderInterface: client.NewReader(inv, contract), - } -} - -func (s *ContractStorageReader) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) { - return listChains(name, target, s.invoker.GetRPCInvoker(), s.hash) -} - -func (s *ContractStorageReader) GetAdmin() (util.Uint160, error) { - return s.contractReaderInterface.GetAdmin() -} - -func (s *ContractStorageReader) ListTargetsIterator(targetType engine.TargetType) (uuid.UUID, result.Iterator, error) { - kind, err := policyKind(targetType) - if err != nil { - return uuid.UUID{}, result.Iterator{}, err - } - return s.contractReaderInterface.ListTargets(big.NewInt(int64(kind))) -} - func bytesFromStackItem(param stackitem.Item) ([]byte, error) { switch param.Type() { case stackitem.BufferT, stackitem.ByteArrayT, stackitem.IntegerT: @@ -233,16 +123,10 @@ func prefixedChainName(name chain.Name, chainID chain.ID) []byte { } func policyKind(typ engine.TargetType) (policy.Kind, error) { - switch typ { - case engine.Namespace: + if typ == engine.Namespace { return policy.Namespace, nil - case engine.Container: + } else if typ == engine.Container { return policy.Container, nil - case engine.User: - return policy.Kind(engine.User), nil - case engine.Group: - return policy.Kind(engine.Group), nil - default: - return policy.Kind(0), ErrEngineTargetTypeUnsupported } + return policy.Kind(0), ErrEngineTargetTypeUnsupported } diff --git a/schema/common/consts.go b/schema/common/consts.go deleted file mode 100644 index 5077a32..0000000 --- a/schema/common/consts.go +++ /dev/null @@ -1,11 +0,0 @@ -package common - -const ( - PropertyKeyFrostFSIDGroupID = "frostfsid:groupID" - - PropertyKeyFrostFSSourceIP = "frostfs:sourceIP" - - PropertyKeyFormatFrostFSIDUserClaim = "frostfsid:userClaim/%s" - - PropertyKeyFrostFSXHeader = "frostfs:xheader/%s" -) diff --git a/schema/native/consts.go b/schema/native/consts.go index bf31002..6277d53 100644 --- a/schema/native/consts.go +++ b/schema/native/consts.go @@ -8,17 +8,6 @@ const ( MethodSearchObject = "SearchObject" MethodRangeObject = "RangeObject" MethodHashObject = "HashObject" - MethodPatchObject = "PatchObject" - - MethodPutContainer = "PutContainer" - MethodDeleteContainer = "DeleteContainer" - MethodGetContainer = "GetContainer" - MethodListContainers = "ListContainers" - MethodSetContainerEACL = "SetContainerEACL" - MethodGetContainerEACL = "GetContainerEACL" - - ObjectPrefix = "native:object" - ContainerPrefix = "native:container" ResourceFormatNamespaceObjects = "native:object/%s/*" ResourceFormatNamespaceContainerObjects = "native:object/%s/%s/*" @@ -38,27 +27,15 @@ const ( ResourceFormatAllContainers = "native:container/*" - PropertyKeyActorPublicKey = "$Actor:publicKey" - PropertyKeyActorRole = "$Actor:role" - - PropertyKeyObjectVersion = "$Object:version" - PropertyKeyObjectID = "$Object:objectID" - PropertyKeyObjectContainerID = "$Object:containerID" - PropertyKeyObjectOwnerID = "$Object:ownerID" - PropertyKeyObjectCreationEpoch = "$Object:creationEpoch" - PropertyKeyObjectPayloadLength = "$Object:payloadLength" - PropertyKeyObjectPayloadHash = "$Object:payloadHash" - PropertyKeyObjectType = "$Object:objectType" - PropertyKeyObjectHomomorphicHash = "$Object:homomorphicHash" - PropertyKeyFormatObjectContainerAttribute = "$Object:containerAttribute/%s" - - PropertyKeyContainerOwnerID = "$Container:ownerID" - PropertyKeyFormatContainerAttribute = "$Container:attribute/%s" - - ProperyKeyTreeID = "$Tree:ID" - - PropertyValueContainerRoleOwner = "owner" - PropertyValueContainerRoleIR = "ir" - PropertyValueContainerRoleContainer = "container" - PropertyValueContainerRoleOthers = "others" + PropertyKeyActorPublicKey = "$Actor:publicKey" + PropertyKeyActorRole = "$Actor:role" + PropertyKeyObjectVersion = "$Object:version" + PropertyKeyObjectID = "$Object:objectID" + PropertyKeyObjectContainerID = "$Object:containerID" + PropertyKeyObjectOwnerID = "$Object:ownerID" + PropertyKeyObjectCreationEpoch = "$Object:creationEpoch" + PropertyKeyObjectPayloadLength = "$Object:payloadLength" + PropertyKeyObjectPayloadHash = "$Object:payloadHash" + PropertyKeyObjectType = "$Object:objectType" + PropertyKeyObjectHomomorphicHash = "$Object:homomorphicHash" ) diff --git a/schema/native/util/validation.go b/schema/native/util/validation.go deleted file mode 100644 index 5ffccb2..0000000 --- a/schema/native/util/validation.go +++ /dev/null @@ -1,45 +0,0 @@ -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 -} diff --git a/schema/native/util/validation_test.go b/schema/native/util/validation_test.go deleted file mode 100644 index 8114935..0000000 --- a/schema/native/util/validation_test.go +++ /dev/null @@ -1,97 +0,0 @@ -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) - } - }) - } -} diff --git a/schema/s3/consts.go b/schema/s3/consts.go index d90b7e8..a8de81f 100644 --- a/schema/s3/consts.go +++ b/schema/s3/consts.go @@ -6,19 +6,4 @@ const ( PropertyKeyDelimiter = "s3:delimiter" PropertyKeyPrefix = "s3:prefix" PropertyKeyVersionID = "s3:VersionId" - PropertyKeyMaxKeys = "s3:max-keys" - - PropertyKeyFormatResourceTag = "aws:ResourceTag/%s" - PropertyKeyFormatRequestTag = "aws:RequestTag/%s" - - PropertyKeyAccessBoxAttrMFA = "AccessBox-Attribute/IAM-MFA" - PropertyKeyFormatAccessBoxAttr = "AccessBox-Attribute/%s" - - 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" )