Compare commits

..

36 commits

Author SHA1 Message Date
a3bc3099bd [#87] iam: Support s3:PatchObject action
Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
2024-08-22 10:41:52 +00:00
ed14db3e66 [#91] lint: Fix warnings
Signed-off-by: Ekaterina Lebedeva <ekaterina.lebedeva@yadro.com>
2024-08-22 12:22:32 +03:00
eb7be61798 [#91] go.mod: Bump go version to 1.22
Signed-off-by: Ekaterina Lebedeva <ekaterina.lebedeva@yadro.com>
2024-08-22 12:22:15 +03:00
a1386f6d25 [#90] engine: Fix ruleFound return value
It can be false if the first targets allows operation and the last one
returns NoRuleFound.

Found by @mbiryukova.
Introduced in #86.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-08-21 07:20:38 +00:00
2300995af2 [#88] iam: Support lifecycle actions in native map
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-08-20 17:17:17 +03:00
a11e80e2c7 [#86] engine: Simplify multiple chains processing
So, it's sunday evening and I am sitting on-call trying to debug strange
node behaviour. It took me 3 whole minutes to understand the code being
changed: it accumulates bools in slices, even though no slice is needed;
it uses subtle condition from the first loop to make decision in the
second one, and finally it uses named return values.

In this commit we remove the slice and the second loop, because why not.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-08-19 09:37:38 +03:00
Airat Arifullin
96225afacb [#85] schema: Introduce PatchObject method
Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2024-08-14 11:02:54 +03:00
Airat Arifullin
2628f61849 [#84] schema: Introduce xheader property
Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2024-07-12 11:14:03 +03:00
ac965e8d17 [#80] iam: Move resource tag to resource property
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-06-11 13:29:30 +03:00
64e06f5b7c [#80] iam: Skip unsupported conditions in native chains
Skip conditions with
* aws:RequestTag
* aws:ResourceTag
keys

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-06-11 11:15:22 +03:00
303a81cdc6 [#78] iam: Don't check IP for private
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-05-27 09:54:02 +03:00
Airat Arifullin
d7ed188f68 [#76] chain: Increase unit-test coverage for chain related types
* Add more unit-test cases.

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2024-05-22 15:08:11 +03:00
aarifullin
1f6f4163d4 [#71] docs: Introduce APE overview
Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2024-05-13 16:37:44 +00:00
Airat Arifullin
84c4872b20 [#75] chain: Refactor ObjectType type
* Rename `ObjectType` to `Kind`;
* Rename `Object` field in `Condition` to `ConditionKind`;
* Regenerate easy-json marshalers/unmarshalers;
* Fix unit-tests

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2024-05-13 17:36:17 +03:00
Airat Arifullin
e75200bb8e [#75] chain: Remove ContaierResource, ContainerRequest constants
Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2024-05-13 13:50:17 +03:00
2e7518c453 [#74] docs: Describe converters
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-05-07 13:10:02 +03:00
2fa27b6557 [#72] chain/test: Refactor fuzz tests
Make it possible to execute fuzz tests with different backend, such as
go-fuzz which supports coverage collection.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-05-03 10:16:35 +00:00
38f947ac0a [#73] pre-commit: Use cached tests in hook
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-05-02 18:15:53 +03:00
34c1eafa56 [#73] Makefile: Allow to override test flags
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-05-02 18:15:53 +03:00
84c15a559c [#73] pre-commit: Remove gitlint hook
It is annoying during local development and is unused on CI.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-05-02 18:15:53 +03:00
aarifullin
c539728641 [#62] morph: List morph rules chains by traversing iterator
* Make `ListMorphRuleChains` methods use `commonclient.ReadIteratorItems`.
* Introduce `ContractStorageActor` interface.
* Iterators are used because listing by `ListChainsByPrefix` may cause
  stack overflow from neo-go side (len(items) > 1024).

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2024-04-26 06:20:43 +00:00
04a79f57ef [#70] iam: Support aws:MultiFactorAuthPresent key
Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
2024-04-16 10:17:28 +03:00
ff5d05ac92 [#67] chain: Support IPAddress conditions
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-04-15 12:52:41 +00:00
0e69e48511 [#64] engine: Add user and group targets
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-04-12 16:07:34 +03:00
530248de75 [#69] iam: Extend native actions with tree service methods
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-04-12 13:22:12 +03:00
b6a6816800 [#68] iam: Allow read object on delete operation
We must be able to read s3 multipart object from storage
(to find out the parts it consists of)
to fully delete such multipart object

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-04-12 09:57:53 +03:00
1f190e1668 [#58] iam: Fix native actions mapping
We have to add native:PutObject when want to delete object
 because of tombstone must be created (it's a put operation)

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-04-10 14:48:23 +03:00
67cf09f51d [#63] iam: Add formatters for resource/request tags
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-04-10 11:14:57 +03:00
84c6be01de [#60] chain: Support numeric conditions
Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
2024-04-08 14:30:43 +03:00
67e4595a91 [#63] iam: Support tag keys
Support:
* aws:PrincipalTag
* aws:ResourceTag
* aws:Request

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-04-05 09:36:01 +03:00
42497ad242 [#59] router: Inmemory implementation should take empty name for "root"
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-04-02 11:09:42 +03:00
1d51f2121d [#58] iam: Support more s3 actions
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-04-01 17:18:20 +03:00
9040e48504 [#57] iam: Add policy validation checks
Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
2024-03-11 16:12:47 +03:00
2ec958cbfd [#56] storage: Allow to remove all chains by target
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2024-03-07 18:11:06 +03:00
8cb2de05ab [#56] Fix pre-commit issue
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2024-03-07 18:01:50 +03:00
aarifullin
c13ed8447a [#52] morph: Extend MorphRuleChainStorage interface with ListTargetsIterator
* Update frostfs-contract package version in go.mod.
* Extend MorphRuleChainStorage interface with ListTargetsIterator and
  introduce its implementation.
* Check targets in inmemory implementation unit-tests.

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2024-03-05 10:17:28 +03:00
41 changed files with 3578 additions and 589 deletions

View file

@ -13,7 +13,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: '1.21' go-version: '1.23'
- name: Run commit format checker - name: Run commit format checker
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v3 uses: https://git.frostfs.info/TrueCloudLab/dco-go@v3

View file

@ -11,7 +11,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: '1.21' go-version: '1.23'
cache: true cache: true
- name: Install linters - name: Install linters
@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
go_versions: [ '1.20', '1.21' ] go_versions: [ '1.22', '1.23' ]
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -48,7 +48,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: '1.21' go-version: '1.23'
cache: true cache: true
- name: Run tests - name: Run tests
@ -63,7 +63,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: '1.21' go-version: '1.23'
cache: true cache: true
- name: Install staticcheck - name: Install staticcheck

View file

@ -12,7 +12,8 @@ run:
# output configuration options # output configuration options
output: output:
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
format: tab formats:
- format: tab
# all available settings of specific linters # all available settings of specific linters
linters-settings: linters-settings:

View file

@ -2,13 +2,6 @@ ci:
autofix_prs: false autofix_prs: false
repos: 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 - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.4.0
hooks: hooks:
@ -42,7 +35,7 @@ repos:
hooks: hooks:
- id: go-unit-tests - id: go-unit-tests
name: go unit tests name: go unit tests
entry: make test entry: make test GOFLAGS=''
pass_filenames: false pass_filenames: false
types: [go] types: [go]
language: system language: system

View file

@ -1,9 +1,9 @@
#!/usr/bin/make -f #!/usr/bin/make -f
TRUECLOUDLAB_LINT_VERSION ?= 0.0.2 TRUECLOUDLAB_LINT_VERSION ?= 0.0.6
TMP_DIR := .cache TMP_DIR := .cache
OUTPUT_LINT_DIR ?= $(shell pwd)/bin OUTPUT_LINT_DIR ?= $(shell pwd)/bin
LINT_VERSION ?= 1.55.1 LINT_VERSION ?= 1.60.1
LINT_DIR = $(OUTPUT_LINT_DIR)/golangci-lint-$(LINT_VERSION)-v$(TRUECLOUDLAB_LINT_VERSION) LINT_DIR = $(OUTPUT_LINT_DIR)/golangci-lint-$(LINT_VERSION)-v$(TRUECLOUDLAB_LINT_VERSION)
EASYJSON_VERSION ?= $(shell go list -f '{{.Version}}' -m github.com/mailru/easyjson) EASYJSON_VERSION ?= $(shell go list -f '{{.Version}}' -m github.com/mailru/easyjson)
EASYJSON_DIR ?= $(shell pwd)/bin/easyjson-$(EASYJSON_VERSION) EASYJSON_DIR ?= $(shell pwd)/bin/easyjson-$(EASYJSON_VERSION)
@ -22,9 +22,10 @@ imports:
@goimports -w . @goimports -w .
# Run Unit Test with go test # Run Unit Test with go test
test: GOFLAGS ?= "-count=1"
test: test:
@echo "⇒ Running go test" @echo "⇒ Running go test"
@go test ./... -count=1 @GOFLAGS="$(GOFLAGS)" go test ./...
# Activate pre-commit hooks # Activate pre-commit hooks
pre-commit: pre-commit:

85
docs/ape.md Normal file
View file

@ -0,0 +1,85 @@
# 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`. | <ul><li>`Operation` - `GetObject`,`PutObject` etc.;</li><li>`Properties` - actor's public key, actor's attributes;</li><li>`Resource`.</li></ul> |
| `Resource` | The object that the request is being performed on. Check also [resource.md](./resource.md). | <ul><li>`Name` - strictly formatted string value;</li><li>`Properties`.</li></ul> |
| `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. | <ul><li>Base64-encoded `ID`;</li><li>List of `Rule`-s;</li><li>`MatchType` - defines rule status selection priority.</li></ul> |
| `Rule` | `Rule` defines which status is returned if `Request` matches all conditions. | <ul><li>`Status`: `Allow`, `AccessDenied`, `QuotaLimitReached`, `NoRuleFound`;</li><li>`Actions` - operation defined by a schema (`GetObject`, `PutContainer` etc.);</li><li>`Resources`;</li><li>`Any` - if `true` then `Reqeust` matches `Rule` if any `Condition` is `true`;</li><li>`Conditions`.</li></ul> |
| `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). | <ul><li>`Namespace`;</li><li>`Container`;</li><li>`User`;</li><li>`Groups`.</li></ul> |
| `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`. | <ul><li>`LocalOverrideStorage` - chains stored in the local override storage have the highest priority</li><li>`MorphRuleChainStorage` - basically, chains stored in `Policy` contract;</li><li>`ChainRouter` - looks up chains and try to match them with `Request`.</li></ul> |
#### 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)

View file

@ -0,0 +1,61 @@
@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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,46 @@
@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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

439
docs/policy_converters.md Normal file
View file

@ -0,0 +1,439 @@
# 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"
}
]
}
```

23
go.mod
View file

@ -1,12 +1,12 @@
module git.frostfs.info/TrueCloudLab/policy-engine module git.frostfs.info/TrueCloudLab/policy-engine
go 1.20 go 1.22
require ( require (
git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958 git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409111539-e7a05a49ff45
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.1
github.com/mailru/easyjson v0.7.7 github.com/mailru/easyjson v0.7.7
github.com/nspcc-dev/neo-go v0.103.0 github.com/nspcc-dev/neo-go v0.105.0
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
) )
@ -14,15 +14,22 @@ require (
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/hashicorp/golang-lru v0.6.0 // indirect github.com/hashicorp/golang-lru v0.6.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect github.com/nspcc-dev/go-ordered-json v0.0.0-20231123160306-3374ff1e7a3c // indirect
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231020160724-c3955f87d1b5 // indirect github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231127165613-b35f351f0ba0 // indirect
github.com/nspcc-dev/rfc6979 v0.2.0 // indirect github.com/nspcc-dev/rfc6979 v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.14.0 // indirect 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/sync v0.3.0 // indirect golang.org/x/sync v0.3.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

149
go.sum
View file

@ -1,49 +1,166 @@
git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958 h1:X9yPizADIhD3K/gdKVCthlAnf9aQ3UJJGnZgIwwixRQ= git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409111539-e7a05a49ff45 h1:Tp4I+XOLp3VCJORfxSamQtj3RZNISbaLM4WD5iIzXxg=
git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958/go.mod h1:rQWdsG18NaiFvkJpMguJev913KD/yleHaniRBkUyt0o= 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=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/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/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 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 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 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 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 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/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 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 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 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 h1:n4ZaFCKt1pQJd7PXoMJabZWK9ejjbLOVrkl/lOUmshg= github.com/nspcc-dev/go-ordered-json v0.0.0-20231123160306-3374ff1e7a3c h1:OOQeE613BH93ICPq3eke5N78gWNeMjcBWkmD2NKyXVg=
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22/go.mod h1:79bEUDEviBHJMFV6Iq6in57FEOCMcRhfQnfaf0ETA5U= github.com/nspcc-dev/go-ordered-json v0.0.0-20231123160306-3374ff1e7a3c/go.mod h1:79bEUDEviBHJMFV6Iq6in57FEOCMcRhfQnfaf0ETA5U=
github.com/nspcc-dev/neo-go v0.103.0 h1:UVyWPhzZdfYFG35ORP3FRDLh8J/raRQ6m8SptDdlgfM= github.com/nspcc-dev/neo-go v0.105.0 h1:vtNZYFEFySK8zRDhLzQYha849VzWrcKezlnq/oNQg/w=
github.com/nspcc-dev/neo-go v0.103.0/go.mod h1:x+wmcYqpZYJwLp1l/pHZrqNp3RSWlkMymWGDij3/OPo= 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-20231020160724-c3955f87d1b5 h1:09CpI5uwsxb1EeFPIKQRwwWlfCmDD/Dwwh01lPiQScM= 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-20231020160724-c3955f87d1b5/go.mod h1:J/Mk6+nKeKSW4wygkZQFLQ6SkLOSGX5Ga0RuuuktEag= github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231127165613-b35f351f0ba0/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 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE=
github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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 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 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo=
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c=
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 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 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= 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=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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=

View file

@ -3,15 +3,77 @@ package iam
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/netip"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode/utf8" "unicode/utf8"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "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 condKeyAWSPrincipalARN = "aws:PrincipalArn" 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"
)
const (
condKeyAWSPrincipalARN = "aws:PrincipalArn"
condKeyAWSSourceIP = "aws:SourceIp"
condKeyAWSPrincipalTagPrefix = "aws:PrincipalTag/"
condKeyAWSRequestTagPrefix = "aws:RequestTag/"
condKeyAWSResourceTagPrefix = "aws:ResourceTag/"
userClaimTagPrefix = "tag-"
)
const ( const (
// String condition operators. // String condition operators.
@ -123,8 +185,8 @@ func convertToChainCondition(c Conditions) ([]GroupedConditions, error) {
group.Conditions[i] = chain.Condition{ group.Conditions[i] = chain.Condition{
Op: condType, Op: condType,
Object: chain.ObjectRequest, Kind: chain.KindRequest,
Key: key, Key: transformKey(key),
Value: converted, Value: converted,
} }
} }
@ -135,6 +197,20 @@ func convertToChainCondition(c Conditions) ([]GroupedConditions, error) {
return grouped, nil 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) { func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFunction, error) {
switch { switch {
case strings.HasPrefix(op, "String"): case strings.HasPrefix(op, "String"):
@ -168,8 +244,7 @@ func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFuncti
return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op) return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op)
} }
case strings.HasPrefix(op, "Numeric"): case strings.HasPrefix(op, "Numeric"):
// TODO return numericConditionTypeAndConverter(op)
return 0, nil, fmt.Errorf("currently nummeric conditions unsupported: '%s'", op)
case strings.HasPrefix(op, "Date"): case strings.HasPrefix(op, "Date"):
switch op { switch op {
case CondDateEquals: case CondDateEquals:
@ -190,13 +265,9 @@ func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFuncti
case op == CondBool: case op == CondBool:
return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil
case op == CondIPAddress: case op == CondIPAddress:
// todo consider using converters return chain.CondIPAddress, ipConvertFunction, nil
// "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: case op == CondNotIPAddress:
return chain.CondStringNotLike, noConvertFunction, nil return chain.CondNotIPAddress, ipConvertFunction, nil
case op == CondSliceContains: case op == CondSliceContains:
return chain.CondSliceContains, noConvertFunction, nil return chain.CondSliceContains, noConvertFunction, nil
default: default:
@ -204,12 +275,50 @@ func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFuncti
} }
} }
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
default:
return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op)
}
}
type convertFunction func(string) (string, error) type convertFunction func(string) (string, error)
func noConvertFunction(val string) (string, error) { func noConvertFunction(val string) (string, error) {
return val, nil 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) { func dateConvertFunction(val string) (string, error) {
if _, err := strconv.ParseInt(val, 10, 64); err == nil { if _, err := strconv.ParseInt(val, 10, 64); err == nil {
return val, nil return val, nil
@ -269,21 +378,18 @@ func validateResource(resource string) error {
return nil return nil
} }
func validateAction(action string) error { func validateAction(action string) (bool, error) {
if action == Wildcard { isIAM := strings.HasPrefix(action, iamActionPrefix)
return nil if !strings.HasPrefix(action, s3ActionPrefix) && !isIAM {
} return false, ErrInvalidActionFormat
if !strings.HasPrefix(action, s3ActionPrefix) && !strings.HasPrefix(action, iamActionPrefix) {
return ErrInvalidActionFormat
} }
index := strings.IndexByte(action, Wildcard[0]) index := strings.IndexByte(action, Wildcard[0])
if index != -1 && index != utf8.RuneCountInString(action)-1 { if index != -1 && index != utf8.RuneCountInString(action)-1 {
return ErrInvalidActionFormat return false, ErrInvalidActionFormat
} }
return nil return isIAM, nil
} }
func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition { func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition {

View file

@ -1,6 +1,7 @@
package iam package iam
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
@ -10,18 +11,56 @@ import (
const PropertyKeyFilePath = "FilePath" const PropertyKeyFilePath = "FilePath"
var supportedActionToNativeOpMap = map[string][]string{ var actionToNativeOpMap = map[string][]string{
supportedS3NativeActionDeleteObject: {native.MethodDeleteObject, native.MethodHeadObject}, s3ActionAbortMultipartUpload: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
supportedS3NativeActionHeadObject: {native.MethodHeadObject}, s3ActionCreateBucket: {native.MethodGetContainer, native.MethodPutContainer, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodPutObject},
supportedS3NativeActionGetObject: {native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, s3ActionDeleteBucket: {native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject, native.MethodGetObject},
supportedS3NativeActionPutObject: {native.MethodPutObject}, s3ActionDeleteBucketPolicy: {native.MethodGetContainer},
supportedS3NativeActionListBucket: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, s3ActionDeleteObject: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject},
s3ActionDeleteObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
supportedS3NativeActionCreateBucket: {native.MethodPutContainer}, s3ActionDeleteObjectVersion: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject},
supportedS3NativeActionDeleteBucket: {native.MethodDeleteContainer}, s3ActionDeleteObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
supportedS3NativeActionListAllMyBucket: {native.MethodListContainers}, s3ActionGetBucketACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject},
supportedS3NativeActionPutBucketACL: {native.MethodSetContainerEACL}, s3ActionGetBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
supportedS3NativeActionGetBucketACL: {native.MethodGetContainerEACL}, 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},
} }
var containerNativeOperations = map[string]struct{}{ var containerNativeOperations = map[string]struct{}{
@ -43,19 +82,7 @@ var objectNativeOperations = map[string]struct{}{
native.MethodHashObject: {}, native.MethodHashObject: {},
} }
const ( var errConditionKeyNotApplicable = errors.New("condition key is not applicable")
supportedS3NativeActionDeleteObject = "s3:DeleteObject"
supportedS3NativeActionGetObject = "s3:GetObject"
supportedS3NativeActionHeadObject = "s3:HeadObject"
supportedS3NativeActionPutObject = "s3:PutObject"
supportedS3NativeActionListBucket = "s3:ListBucket"
supportedS3NativeActionCreateBucket = "s3:CreateBucket"
supportedS3NativeActionDeleteBucket = "s3:DeleteBucket"
supportedS3NativeActionListAllMyBucket = "s3:ListAllMyBuckets"
supportedS3NativeActionPutBucketACL = "s3:PutBucketAcl"
supportedS3NativeActionGetBucketACL = "s3:GetBucketAcl"
)
type NativeResolver interface { type NativeResolver interface {
GetUserKey(account, name string) (string, error) GetUserKey(account, name string) (string, error)
@ -76,6 +103,11 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro
for _, statement := range p.Statement { for _, statement := range p.Statement {
status := formStatus(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() action, actionInverted := statement.action()
nativeActions, err := formNativeActionNames(action) nativeActions, err := formNativeActionNames(action)
@ -95,6 +127,9 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro
groupedConditions, err := convertToNativeChainCondition(statement.Conditions, resolver) groupedConditions, err := convertToNativeChainCondition(statement.Conditions, resolver)
if err != nil { if err != nil {
if errors.Is(err, errConditionKeyNotApplicable) {
continue
}
return nil, err return nil, err
} }
splitConditions := splitGroupedConditions(groupedConditions) splitConditions := splitGroupedConditions(groupedConditions)
@ -146,8 +181,8 @@ func getActionTypes(nativeActions []string) ActionTypes {
_, isObj := objectNativeOperations[action] _, isObj := objectNativeOperations[action]
_, isCnr := containerNativeOperations[action] _, isCnr := containerNativeOperations[action]
res.Object = isObj || action == Wildcard res.Object = res.Object || isObj || action == Wildcard
res.Container = isCnr || action == Wildcard res.Container = res.Container || isCnr || action == Wildcard
} }
return res return res
@ -181,7 +216,7 @@ func getNativePrincipalsAndConditionFunc(statement Statement, resolver NativeRes
return principals, func(principal string) chain.Condition { return principals, func(principal string) chain.Condition {
return chain.Condition{ return chain.Condition{
Op: op, Op: op,
Object: chain.ObjectRequest, Kind: chain.KindRequest,
Key: native.PropertyKeyActorPublicKey, Key: native.PropertyKeyActorPublicKey,
Value: principal, Value: principal,
} }
@ -190,18 +225,33 @@ func getNativePrincipalsAndConditionFunc(statement Statement, resolver NativeRes
func convertToNativeChainCondition(c Conditions, resolver NativeResolver) ([]GroupedConditions, error) { func convertToNativeChainCondition(c Conditions, resolver NativeResolver) ([]GroupedConditions, error) {
return convertToChainConditions(c, func(gr GroupedConditions) (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 { for i := range gr.Conditions {
if gr.Conditions[i].Key == condKeyAWSPrincipalARN { switch {
case gr.Conditions[i].Key == condKeyAWSMFAPresent:
return GroupedConditions{}, errConditionKeyNotApplicable
case gr.Conditions[i].Key == condKeyAWSPrincipalARN:
gr.Conditions[i].Key = native.PropertyKeyActorPublicKey gr.Conditions[i].Key = native.PropertyKeyActorPublicKey
val, err := formPrincipalKey(gr.Conditions[i].Value, resolver) val, err := formPrincipalKey(gr.Conditions[i].Value, resolver)
if err != nil { if err != nil {
return GroupedConditions{}, err return GroupedConditions{}, err
} }
gr.Conditions[i].Value = val 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 gr, nil return res, nil
}) })
} }
@ -222,7 +272,7 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve
res := make([]GroupedResources, 0, len(names)) res := make([]GroupedResources, 0, len(names))
var combined []string combined := make(map[string]struct{})
for _, resource := range names { for _, resource := range names {
if err := validateResource(resource); err != nil { if err := validateResource(resource); err != nil {
@ -261,20 +311,24 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve
} }
if obj == Wildcard && actionTypes.Object { // this corresponds to arn:aws:s3:::BUCKET/ or arn:aws:s3:::BUCKET/* if obj == Wildcard && actionTypes.Object { // this corresponds to arn:aws:s3:::BUCKET/ or arn:aws:s3:::BUCKET/*
combined = append(combined, fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)) combined[fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
continue continue
} }
if obj == "" && actionTypes.Container { // this corresponds to arn:aws:s3:::BUCKET if obj == "" && actionTypes.Container { // this corresponds to arn:aws:s3:::BUCKET
combined = append(combined, fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)) combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
continue continue
} }
res = append(res, GroupedResources{ res = append(res, GroupedResources{
Names: []string{fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)}, Names: []string{
fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container),
fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container),
},
Conditions: []chain.Condition{ Conditions: []chain.Condition{
{ {
Op: chain.CondStringLike, Op: chain.CondStringLike,
Object: chain.ObjectResource, Kind: chain.KindResource,
Key: PropertyKeyFilePath, Key: PropertyKeyFilePath,
Value: obj, Value: obj,
}, },
@ -283,7 +337,12 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve
} }
if len(combined) != 0 { if len(combined) != 0 {
res = append(res, GroupedResources{Names: combined}) gr := GroupedResources{Names: make([]string, 0, len(combined))}
for key := range combined {
gr.Names = append(gr.Names, key)
}
res = append(res, gr)
} }
return res, nil return res, nil
@ -329,26 +388,39 @@ func formPrincipalKey(principal string, resolver NativeResolver) (string, error)
} }
func formNativeActionNames(names []string) ([]string, error) { func formNativeActionNames(names []string) ([]string, error) {
res := make([]string, 0, len(names)) uniqueActions := make(map[string]struct{}, len(names))
for _, action := range names { for _, action := range names {
if err := validateAction(action); err != nil {
return nil, err
}
if action == Wildcard { if action == Wildcard {
return []string{Wildcard}, nil return []string{Wildcard}, nil
} }
if !strings.HasPrefix(action, s3ActionPrefix) { isIAM, err := validateAction(action)
if err != nil {
return nil, err
}
if isIAM {
continue continue
} }
if strings.TrimPrefix(action, s3ActionPrefix) == Wildcard { if action[len(s3ActionPrefix):] == Wildcard {
return []string{Wildcard}, nil return []string{Wildcard}, nil
} }
res = append(res, supportedActionToNativeOpMap[action]...) 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)
} }
return res, nil return res, nil

View file

@ -2,81 +2,71 @@ package iam
import ( import (
"fmt" "fmt"
"strings"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3" "git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
) )
var specialActionToS3OpMap = map[string][]string{ const condKeyAWSMFAPresent = "aws:MultiFactorAuthPresent"
specialS3ActionsListAllMyBuckets: {"s3:ListBuckets"},
specialS3ActionsListBucket: {"s3:HeadBucket", "s3:GetBucketLocation", "s3:ListObjectsV1", "s3:ListObjectsV2"},
specialS3ActionsListBucketVersions: {"s3:ListBucketObjectVersions"},
specialS3ActionsListBucketMultipartUploads: {"s3:ListMultipartUploads"},
specialS3ActionsGetBucketObjectLockConfiguration: {"s3:GetBucketObjectLockConfig"},
specialS3ActionsGetEncryptionConfiguration: {"s3:GetBucketEncryption"},
specialS3ActionsGetLifecycleConfiguration: {"s3:GetBucketLifecycle"},
specialS3ActionsGetBucketACL: {"s3:GetBucketACL"},
specialS3ActionsGetBucketCORS: {"s3:GetBucketCors"},
specialS3ActionsPutBucketTagging: {"s3:PutBucketTagging", "s3:DeleteBucketTagging"},
specialS3ActionsPutBucketObjectLockConfiguration: {"s3:PutBucketObjectLockConfig"},
specialS3ActionsPutEncryptionConfiguration: {"s3:PutBucketEncryption", "s3:DeleteBucketEncryption"},
specialS3ActionsPutLifecycleConfiguration: {"s3:PutBucketLifecycle", "s3:DeleteBucketLifecycle"},
specialS3ActionsPutBucketACL: {"s3:PutBucketACL"},
specialS3ActionsPutBucketCORS: {"s3:PutBucketCors", "s3:DeleteBucketCors"},
specialS3ActionsDeleteBucketCORS: {"s3:DeleteBucketCors"},
specialS3ActionsListMultipartUploadParts: {"s3:ListParts"}, var actionToS3OpMap = map[string][]string{
specialS3ActionsGetObjectACL: {"s3:GetObjectACL"}, s3ActionAbortMultipartUpload: {s3ActionAbortMultipartUpload},
specialS3ActionsGetObject: {"s3:GetObject", "s3:HeadObject"}, s3ActionCreateBucket: {s3ActionCreateBucket},
specialS3ActionsGetObjectVersion: {"s3:GetObject", "s3:HeadObject"}, s3ActionDeleteBucket: {s3ActionDeleteBucket},
specialS3ActionsGetObjectVersionACL: {"s3:GetObjectACL"}, s3ActionDeleteBucketPolicy: {s3ActionDeleteBucketPolicy},
specialS3ActionsGetObjectVersionAttributes: {"s3:GetObjectAttributes"}, s3ActionDeleteObjectTagging: {s3ActionDeleteObjectTagging},
specialS3ActionsGetObjectVersionTagging: {"s3:GetObjectTagging"}, s3ActionGetBucketLocation: {s3ActionGetBucketLocation},
specialS3ActionsPutObjectACL: {"s3:PutObjectACL"}, s3ActionGetBucketNotification: {s3ActionGetBucketNotification},
specialS3ActionsPutObjectVersionACL: {"s3:PutObjectACL"}, s3ActionGetBucketPolicy: {s3ActionGetBucketPolicy},
specialS3ActionsPutObjectVersionTagging: {"s3:PutObjectTagging"}, s3ActionGetBucketPolicyStatus: {s3ActionGetBucketPolicyStatus},
specialS3ActionsPutObject: { 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:PutObject", "s3:PostObject", "s3:CopyObject",
"s3:UploadPart", "s3:UploadPartCopy", "s3:CreateMultipartUpload", "s3:CompleteMultipartUpload", "s3:UploadPart", "s3:UploadPartCopy", "s3:CreateMultipartUpload", "s3:CompleteMultipartUpload",
}, },
specialS3ActionsDeleteObjectVersionTagging: {"s3:DeleteObjectTagging"}, s3ActionDeleteObjectVersionTagging: {"s3:DeleteObjectTagging"},
specialS3ActionsDeleteObject: {"s3:DeleteObject", "s3:DeleteMultipleObjects"}, s3ActionDeleteObject: {"s3:DeleteObject", "s3:DeleteMultipleObjects"},
specialS3ActionsDeleteObjectVersion: {"s3:DeleteObject", "s3:DeleteMultipleObjects"}, s3ActionDeleteObjectVersion: {"s3:DeleteObject", "s3:DeleteMultipleObjects"},
} }
const (
specialS3ActionsListAllMyBuckets = "s3:ListAllMyBuckets"
specialS3ActionsListBucket = "s3:ListBucket"
specialS3ActionsListBucketVersions = "s3:ListBucketVersions"
specialS3ActionsListBucketMultipartUploads = "s3:ListBucketMultipartUploads"
specialS3ActionsGetBucketObjectLockConfiguration = "s3:GetBucketObjectLockConfiguration"
specialS3ActionsGetEncryptionConfiguration = "s3:GetEncryptionConfiguration"
specialS3ActionsGetLifecycleConfiguration = "s3:GetLifecycleConfiguration"
specialS3ActionsGetBucketACL = "s3:GetBucketAcl"
specialS3ActionsGetBucketCORS = "s3:GetBucketCORS"
specialS3ActionsPutBucketTagging = "s3:PutBucketTagging"
specialS3ActionsPutBucketObjectLockConfiguration = "s3:PutBucketObjectLockConfiguration"
specialS3ActionsPutEncryptionConfiguration = "s3:PutEncryptionConfiguration"
specialS3ActionsPutLifecycleConfiguration = "s3:PutLifecycleConfiguration"
specialS3ActionsPutBucketACL = "s3:PutBucketAcl"
specialS3ActionsPutBucketCORS = "s3:PutBucketCORS"
specialS3ActionsDeleteBucketCORS = "s3:DeleteBucketCORS"
specialS3ActionsListMultipartUploadParts = "s3:ListMultipartUploadParts"
specialS3ActionsGetObjectACL = "s3:GetObjectAcl"
specialS3ActionsGetObject = "s3:GetObject"
specialS3ActionsGetObjectVersion = "s3:GetObjectVersion"
specialS3ActionsGetObjectVersionACL = "s3:GetObjectVersionAcl"
specialS3ActionsGetObjectVersionAttributes = "s3:GetObjectVersionAttributes"
specialS3ActionsGetObjectVersionTagging = "s3:GetObjectVersionTagging"
specialS3ActionsPutObjectACL = "s3:PutObjectAcl"
specialS3ActionsPutObjectVersionACL = "s3:PutObjectVersionAcl"
specialS3ActionsPutObjectVersionTagging = "s3:PutObjectVersionTagging"
specialS3ActionsPutObject = "s3:PutObject"
specialS3ActionsDeleteObjectVersionTagging = "s3:DeleteObjectVersionTagging"
specialS3ActionsDeleteObject = "s3:DeleteObject"
specialS3ActionsDeleteObjectVersion = "s3:DeleteObjectVersion"
)
type S3Resolver interface { type S3Resolver interface {
GetUserAddress(account, user string) (string, error) GetUserAddress(account, user string) (string, error)
} }
@ -171,7 +161,7 @@ func getS3PrincipalsAndConditionFunc(statement Statement, resolver S3Resolver) (
return principals, func(principal string) chain.Condition { return principals, func(principal string) chain.Condition {
return chain.Condition{ return chain.Condition{
Op: op, Op: op,
Object: chain.ObjectRequest, Kind: chain.KindRequest,
Key: s3.PropertyKeyOwner, Key: s3.PropertyKeyOwner,
Value: principal, Value: principal,
} }
@ -181,13 +171,19 @@ func getS3PrincipalsAndConditionFunc(statement Statement, resolver S3Resolver) (
func convertToS3ChainCondition(c Conditions, resolver S3Resolver) ([]GroupedConditions, error) { func convertToS3ChainCondition(c Conditions, resolver S3Resolver) ([]GroupedConditions, error) {
return convertToChainConditions(c, func(gr GroupedConditions) (GroupedConditions, error) { return convertToChainConditions(c, func(gr GroupedConditions) (GroupedConditions, error) {
for i := range gr.Conditions { for i := range gr.Conditions {
if gr.Conditions[i].Key == condKeyAWSPrincipalARN { switch {
case gr.Conditions[i].Key == condKeyAWSPrincipalARN:
gr.Conditions[i].Key = s3.PropertyKeyOwner gr.Conditions[i].Key = s3.PropertyKeyOwner
val, err := formPrincipalOwner(gr.Conditions[i].Value, resolver) val, err := formPrincipalOwner(gr.Conditions[i].Value, resolver)
if err != nil { if err != nil {
return GroupedConditions{}, err return GroupedConditions{}, err
} }
gr.Conditions[i].Value = val 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
} }
} }
@ -233,22 +229,41 @@ func validateS3ResourceNames(names []string) error {
} }
func formS3ActionNames(names []string) ([]string, error) { func formS3ActionNames(names []string) ([]string, error) {
res := make([]string, 0, len(names)) uniqueActions := make(map[string]struct{}, len(names))
for _, action := range names { for _, action := range names {
if err := validateAction(action); err != nil {
return nil, err
}
if action == Wildcard { if action == Wildcard {
return []string{Wildcard}, nil return []string{Wildcard}, nil
} }
if actions, ok := specialActionToS3OpMap[action]; ok { isIAM, err := validateAction(action)
res = append(res, actions...) if err != nil {
} else { return nil, err
res = append(res, action)
} }
if isIAM {
uniqueActions[action] = struct{}{}
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)
} }
return res, nil return res, nil

File diff suppressed because it is too large Load diff

View file

@ -46,6 +46,8 @@ type (
PrincipalType string PrincipalType string
) )
const policyVersion = "2012-10-17"
const ( const (
GeneralPolicyType PolicyType = iota GeneralPolicyType PolicyType = iota
IdentityBasedPolicyType IdentityBasedPolicyType
@ -222,11 +224,20 @@ func (p Policy) Validate(typ PolicyType) error {
} }
func (p Policy) validate() 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 { if len(p.Statement) == 0 {
return errors.New("'Statement' is missing") return errors.New("'Statement' is missing")
} }
sids := make(map[string]struct{}, len(p.Statement))
for _, statement := range 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() { if !statement.Effect.IsValid() {
return fmt.Errorf("unknown effect: '%s'", statement.Effect) return fmt.Errorf("unknown effect: '%s'", statement.Effect)
} }

View file

@ -2,14 +2,13 @@ package iam
import ( import (
"encoding/json" "encoding/json"
"fmt"
"testing" "testing"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native" "git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -218,6 +217,7 @@ func TestValidatePolicies(t *testing.T) {
{ {
name: "valid permission boundaries", name: "valid permission boundaries",
policy: Policy{ policy: Policy{
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: AllowEffect, Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
@ -230,6 +230,7 @@ func TestValidatePolicies(t *testing.T) {
{ {
name: "general invalid effect", name: "general invalid effect",
policy: Policy{ policy: Policy{
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: "dummy", Effect: "dummy",
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
@ -242,6 +243,7 @@ func TestValidatePolicies(t *testing.T) {
{ {
name: "general invalid principal block", name: "general invalid principal block",
policy: Policy{ policy: Policy{
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: AllowEffect, Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
@ -256,6 +258,7 @@ func TestValidatePolicies(t *testing.T) {
{ {
name: "general invalid not principal", name: "general invalid not principal",
policy: Policy{ policy: Policy{
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: AllowEffect, Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
@ -269,6 +272,7 @@ func TestValidatePolicies(t *testing.T) {
{ {
name: "general invalid principal type", name: "general invalid principal type",
policy: Policy{ policy: Policy{
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: AllowEffect, Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
@ -282,6 +286,7 @@ func TestValidatePolicies(t *testing.T) {
{ {
name: "general invalid action block", name: "general invalid action block",
policy: Policy{ policy: Policy{
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: AllowEffect, Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
@ -295,6 +300,7 @@ func TestValidatePolicies(t *testing.T) {
{ {
name: "general invalid resource block", name: "general invalid resource block",
policy: Policy{ policy: Policy{
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: AllowEffect, Effect: AllowEffect,
Resource: []string{Wildcard}, Resource: []string{Wildcard},
@ -307,6 +313,7 @@ func TestValidatePolicies(t *testing.T) {
{ {
name: "invalid resource block", name: "invalid resource block",
policy: Policy{ policy: Policy{
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: AllowEffect, Effect: AllowEffect,
Resource: []string{}, Resource: []string{},
@ -319,6 +326,7 @@ func TestValidatePolicies(t *testing.T) {
{ {
name: "missing resource block", name: "missing resource block",
policy: Policy{ policy: Policy{
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: AllowEffect, Effect: AllowEffect,
}}, }},
@ -332,9 +340,43 @@ func TestValidatePolicies(t *testing.T) {
typ: GeneralPolicyType, typ: GeneralPolicyType,
isValid: false, 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", name: "identity based valid",
policy: Policy{ policy: Policy{
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: AllowEffect, Effect: AllowEffect,
Action: []string{"s3:PutObject"}, Action: []string{"s3:PutObject"},
@ -348,6 +390,7 @@ func TestValidatePolicies(t *testing.T) {
name: "identity based invalid because of id presence", name: "identity based invalid because of id presence",
policy: Policy{ policy: Policy{
ID: "some-id", ID: "some-id",
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: AllowEffect, Effect: AllowEffect,
Action: []string{"s3:PutObject"}, Action: []string{"s3:PutObject"},
@ -360,6 +403,7 @@ func TestValidatePolicies(t *testing.T) {
{ {
name: "identity based invalid because of principal presence", name: "identity based invalid because of principal presence",
policy: Policy{ policy: Policy{
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: AllowEffect, Effect: AllowEffect,
Action: []string{"s3:PutObject"}, Action: []string{"s3:PutObject"},
@ -373,6 +417,7 @@ func TestValidatePolicies(t *testing.T) {
{ {
name: "identity based invalid because of not principal presence", name: "identity based invalid because of not principal presence",
policy: Policy{ policy: Policy{
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: AllowEffect, Effect: AllowEffect,
Action: []string{"s3:PutObject"}, Action: []string{"s3:PutObject"},
@ -386,6 +431,7 @@ func TestValidatePolicies(t *testing.T) {
{ {
name: "resource based valid principal", name: "resource based valid principal",
policy: Policy{ policy: Policy{
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: DenyEffect, Effect: DenyEffect,
Action: []string{"s3:PutObject"}, Action: []string{"s3:PutObject"},
@ -400,6 +446,7 @@ func TestValidatePolicies(t *testing.T) {
name: "resource based valid not principal", name: "resource based valid not principal",
policy: Policy{ policy: Policy{
ID: "some-id", ID: "some-id",
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: DenyEffect, Effect: DenyEffect,
Action: []string{"s3:PutObject"}, Action: []string{"s3:PutObject"},
@ -414,6 +461,7 @@ func TestValidatePolicies(t *testing.T) {
name: "resource based invalid missing principal", name: "resource based invalid missing principal",
policy: Policy{ policy: Policy{
ID: "some-id", ID: "some-id",
Version: policyVersion,
Statement: []Statement{{ Statement: []Statement{{
Effect: AllowEffect, Effect: AllowEffect,
Action: []string{"s3:PutObject"}, Action: []string{"s3:PutObject"},
@ -476,27 +524,27 @@ func TestProcessDenyFirst(t *testing.T) {
mockResolver := newMockUserResolver([]string{"root/user-name"}, []string{"test-bucket"}, "") mockResolver := newMockUserResolver([]string{"root/user-name"}, []string{"test-bucket"}, "")
identityNativePolicy, err := ConvertToNativeChain(identityPolicy, mockResolver) identityNativePolicy, err := ConvertToS3Chain(identityPolicy, mockResolver)
require.NoError(t, err) require.NoError(t, err)
identityNativePolicy.MatchType = chain.MatchTypeFirstMatch identityNativePolicy.MatchType = chain.MatchTypeFirstMatch
resourceNativePolicy, err := ConvertToNativeChain(resourcePolicy, mockResolver) resourceNativePolicy, err := ConvertToS3Chain(resourcePolicy, mockResolver)
require.NoError(t, err) require.NoError(t, err)
s := inmemory.NewInMemory() s := inmemory.NewInMemory()
target := engine.NamespaceTarget("ns") target := engine.NamespaceTarget("ns")
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, identityNativePolicy) _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, target, identityNativePolicy)
require.NoError(t, err) require.NoError(t, err)
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, resourceNativePolicy) _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, target, resourceNativePolicy)
require.NoError(t, err) require.NoError(t, err)
resource := testutil.NewResource(fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers["test-bucket"]), nil) resource := testutil.NewResource("arn:aws:s3:::test-bucket/object", nil)
request := testutil.NewRequest("PutObject", resource, map[string]string{native.PropertyKeyActorPublicKey: mockResolver.users["root/user-name"]}) request := testutil.NewRequest("s3:PutObject", resource, map[string]string{s3.PropertyKeyOwner: mockResolver.users["root/user-name"]})
status, found, err := s.IsAllowed(chain.Ingress, engine.NewRequestTarget("ns", ""), request) status, found, err := s.IsAllowed(chain.S3, engine.NewRequestTarget("ns", ""), request)
require.NoError(t, err) require.NoError(t, err)
require.True(t, found) require.True(t, found)
require.Equal(t, chain.AccessDenied, status) require.Equal(t, chain.AccessDenied, status)

View file

@ -2,10 +2,12 @@ package chain
import ( import (
"fmt" "fmt"
"net/netip"
"strings" "strings"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
"git.frostfs.info/TrueCloudLab/policy-engine/util" "git.frostfs.info/TrueCloudLab/policy-engine/util"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@ -67,18 +69,16 @@ type Resources struct {
type Condition struct { type Condition struct {
Op ConditionType Op ConditionType
Object ObjectType Kind ConditionKindType
Key string Key string
Value string Value string
} }
type ObjectType byte type ConditionKindType byte
const ( const (
ObjectResource ObjectType = iota KindResource ConditionKindType = iota
ObjectRequest KindRequest
ContainerResource
ContainerRequest
) )
type ConditionType byte type ConditionType byte
@ -108,6 +108,9 @@ const (
CondNumericGreaterThanEquals CondNumericGreaterThanEquals
CondSliceContains CondSliceContains
CondIPAddress
CondNotIPAddress
) )
var condToStr = []struct { var condToStr = []struct {
@ -131,6 +134,8 @@ var condToStr = []struct {
{CondNumericGreaterThan, "NumericGreaterThan"}, {CondNumericGreaterThan, "NumericGreaterThan"},
{CondNumericGreaterThanEquals, "NumericGreaterThanEquals"}, {CondNumericGreaterThanEquals, "NumericGreaterThanEquals"},
{CondSliceContains, "SliceContains"}, {CondSliceContains, "SliceContains"},
{CondIPAddress, "IPAddress"},
{CondNotIPAddress, "NotIPAddress"},
} }
func (c ConditionType) String() string { func (c ConditionType) String() string {
@ -152,13 +157,13 @@ func FormCondSliceContainsValue(values []string) string {
func (c *Condition) Match(req resource.Request) bool { func (c *Condition) Match(req resource.Request) bool {
var val string var val string
switch c.Object { switch c.Kind {
case ObjectResource: case KindResource:
val = req.Resource().Property(c.Key) val = req.Resource().Property(c.Key)
case ObjectRequest: case KindRequest:
val = req.Property(c.Key) val = req.Property(c.Key)
default: default:
panic(fmt.Sprintf("unknown condition type: %d", c.Object)) panic(fmt.Sprintf("unknown condition type: %d", c.Kind))
} }
switch c.Op { switch c.Op {
@ -186,6 +191,61 @@ func (c *Condition) Match(req resource.Request) bool {
return val >= c.Value return val >= c.Value
case CondSliceContains: case CondSliceContains:
return slices.Contains(strings.Split(val, condSliceContainsDelimiter), c.Value) 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)
} }
} }

Binary file not shown.

View file

@ -1,11 +1,14 @@
package chain package chain
import ( import (
"fmt"
"strings"
"testing" "testing"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/common" "git.frostfs.info/TrueCloudLab/policy-engine/schema/common"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native" "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -49,7 +52,7 @@ func TestEncodeDecode(t *testing.T) {
require.Equal(t, expected, actual) require.Equal(t, expected, actual)
} }
func TestReturnFirstMatch(t *testing.T) { func TestChainMatch(t *testing.T) {
ch := Chain{ ch := Chain{
Rules: []Rule{ Rules: []Rule{
{ {
@ -86,9 +89,195 @@ func TestReturnFirstMatch(t *testing.T) {
require.True(t, found) require.True(t, found)
require.Equal(t, Allow, st) 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 TestCondSliceContainsMatch(t *testing.T) { 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 propKey := common.PropertyKeyFrostFSIDGroupID
groupID := "1" groupID := "1"
@ -98,7 +287,7 @@ func TestCondSliceContainsMatch(t *testing.T) {
Resources: Resources{Names: []string{native.ResourceFormatRootContainers}}, Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
Condition: []Condition{{ Condition: []Condition{{
Op: CondSliceContains, Op: CondSliceContains,
Object: ObjectRequest, Kind: KindRequest,
Key: propKey, Key: propKey,
Value: groupID, Value: groupID,
}}, }},
@ -149,3 +338,691 @@ func TestCondSliceContainsMatch(t *testing.T) {
}) })
} }
} }
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)
})
}

View file

@ -218,7 +218,7 @@ func marshalCondition(buf []byte, offset int, c Condition) (int, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
offset, err = marshal.ByteMarshal(buf, offset, byte(c.Object)) offset, err = marshal.ByteMarshal(buf, offset, byte(c.Kind))
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -241,7 +241,7 @@ func unmarshalCondition(buf []byte, offset int) (Condition, int, error) {
if err != nil { if err != nil {
return Condition{}, 0, err return Condition{}, 0, err
} }
c.Object = ObjectType(obV) c.Kind = ConditionKindType(obV)
c.Key, offset, err = marshal.StringUnmarshal(buf, offset) c.Key, offset, err = marshal.StringUnmarshal(buf, offset)
if err != nil { if err != nil {

View file

@ -28,31 +28,6 @@ func TestInvalidChainData(t *testing.T) {
require.Error(t, ch.UnmarshalBinary([]byte("\x00\x00:aws:iam::namespace:group/so\x82\x82\x82\x82\x82\x82u\x82"))) require.Error(t, ch.UnmarshalBinary([]byte("\x00\x00:aws:iam::namespace:group/so\x82\x82\x82\x82\x82\x82u\x82")))
} }
func FuzzUnmarshal(f *testing.F) {
for _, id := range generateTestIDs() {
for _, rules := range generateTestRules() {
for _, matchType := range generateTestMatchTypes() {
chain := Chain{
ID: id,
Rules: rules,
MatchType: matchType,
}
data, err := chain.MarshalBinary()
require.NoError(f, err)
f.Add(data)
}
}
}
f.Fuzz(func(t *testing.T, data []byte) {
var ch Chain
require.NotPanics(t, func() {
_ = ch.UnmarshalBinary(data)
})
})
}
func performMarshalTest(t *testing.T, id ID, r []Rule, mt MatchType) { func performMarshalTest(t *testing.T, id ID, r []Rule, mt MatchType) {
chain := Chain{ chain := Chain{
ID: id, ID: id,
@ -204,28 +179,28 @@ func generateTestConditions() [][]Condition {
for _, ot := range generateObjectTypes() { for _, ot := range generateObjectTypes() {
result[2] = append(result[2], Condition{ result[2] = append(result[2], Condition{
Op: ct, Op: ct,
Object: ot, Kind: ot,
Key: "", Key: "",
Value: "", Value: "",
}) })
result[2] = append(result[2], Condition{ result[2] = append(result[2], Condition{
Op: ct, Op: ct,
Object: ot, Kind: ot,
Key: "key", Key: "key",
Value: "", Value: "",
}) })
result[2] = append(result[2], Condition{ result[2] = append(result[2], Condition{
Op: ct, Op: ct,
Object: ot, Kind: ot,
Key: "", Key: "",
Value: "value", Value: "value",
}) })
result[2] = append(result[2], Condition{ result[2] = append(result[2], Condition{
Op: ct, Op: ct,
Object: ot, Kind: ot,
Key: "key", Key: "key",
Value: "value", Value: "value",
}) })
@ -257,10 +232,10 @@ func generateTestConditionTypes() []ConditionType {
} }
} }
func generateObjectTypes() []ObjectType { func generateObjectTypes() []ConditionKindType {
return []ObjectType{ return []ConditionKindType{
ObjectResource, KindResource,
ObjectRequest, KindRequest,
} }
} }

13
pkg/chain/marshal_fuzz.go Normal file
View file

@ -0,0 +1,13 @@
//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
}

View file

@ -0,0 +1,34 @@
//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)
})
})
}

View file

@ -29,11 +29,11 @@ var statusToJSONValue = []struct {
} }
var objectTypeToJSONValue = []struct { var objectTypeToJSONValue = []struct {
t ObjectType t ConditionKindType
str string str string
}{ }{
{ObjectRequest, "Request"}, {KindRequest, "Request"},
{ObjectResource, "Resource"}, {KindResource, "Resource"},
} }
func (mt MatchType) MarshalEasyJSON(w *jwriter.Writer) { func (mt MatchType) MarshalEasyJSON(w *jwriter.Writer) {
@ -90,7 +90,7 @@ func (st *Status) UnmarshalEasyJSON(l *jlexer.Lexer) {
*st = Status(v) *st = Status(v)
} }
func (ot ObjectType) MarshalEasyJSON(w *jwriter.Writer) { func (ot ConditionKindType) MarshalEasyJSON(w *jwriter.Writer) {
for _, p := range objectTypeToJSONValue { for _, p := range objectTypeToJSONValue {
if p.t == ot { if p.t == ot {
w.String(p.str) w.String(p.str)
@ -100,7 +100,7 @@ func (ot ObjectType) MarshalEasyJSON(w *jwriter.Writer) {
w.String(strconv.FormatUint(uint64(ot), 10)) w.String(strconv.FormatUint(uint64(ot), 10))
} }
func (ot *ObjectType) UnmarshalEasyJSON(l *jlexer.Lexer) { func (ot *ConditionKindType) UnmarshalEasyJSON(l *jlexer.Lexer) {
str := l.String() str := l.String()
for _, p := range objectTypeToJSONValue { for _, p := range objectTypeToJSONValue {
if p.str == str { if p.str == str {
@ -114,7 +114,7 @@ func (ot *ObjectType) UnmarshalEasyJSON(l *jlexer.Lexer) {
l.AddError(fmt.Errorf("failed to parse object type: %w", err)) l.AddError(fmt.Errorf("failed to parse object type: %w", err))
return return
} }
*ot = ObjectType(v) *ot = ConditionKindType(v)
} }
func (ct ConditionType) MarshalEasyJSON(w *jwriter.Writer) { func (ct ConditionType) MarshalEasyJSON(w *jwriter.Writer) {

View file

@ -69,7 +69,7 @@ func TestJsonEnums(t *testing.T) {
Condition: []Condition{ Condition: []Condition{
{ {
Op: CondStringEquals, Op: CondStringEquals,
Object: ObjectRequest, Kind: KindRequest,
Key: native.PropertyKeyActorRole, Key: native.PropertyKeyActorRole,
Value: native.PropertyValueContainerRoleOthers, Value: native.PropertyValueContainerRoleOthers,
}, },
@ -88,7 +88,7 @@ func TestJsonEnums(t *testing.T) {
Condition: []Condition{ Condition: []Condition{
{ {
Op: CondStringNotLike, Op: CondStringNotLike,
Object: ObjectResource, Kind: KindResource,
Key: native.PropertyKeyObjectType, Key: native.PropertyKeyObjectType,
Value: "regular", Value: "regular",
}, },
@ -99,7 +99,7 @@ func TestJsonEnums(t *testing.T) {
Condition: []Condition{ Condition: []Condition{
{ {
Op: ConditionType(255), Op: ConditionType(255),
Object: ObjectType(128), Kind: ConditionKindType(128),
}, },
}, },
}, },

View file

@ -20,7 +20,7 @@
"Condition": [ "Condition": [
{ {
"Op": "StringEquals", "Op": "StringEquals",
"Object": "Request", "Kind": "Request",
"Key": "$Actor:role", "Key": "$Actor:role",
"Value": "others" "Value": "others"
} }
@ -44,7 +44,7 @@
"Condition": [ "Condition": [
{ {
"Op": "StringNotLike", "Op": "StringNotLike",
"Object": "Resource", "Kind": "Resource",
"Key": "$Object:objectType", "Key": "$Object:objectType",
"Value": "regular" "Value": "regular"
} }
@ -64,7 +64,7 @@
"Condition": [ "Condition": [
{ {
"Op": "255", "Op": "255",
"Object": "128", "Kind": "128",
"Key": "", "Key": "",
"Value": "" "Value": ""
} }

View file

@ -42,43 +42,36 @@ func (dr *defaultChainRouter) checkLocal(name chain.Name, rt RequestTarget, r re
if dr.local == nil { if dr.local == nil {
return return
} }
var ruleFounds []bool
var hasAllow bool
for _, target := range rt.Targets() { for _, target := range rt.Targets() {
status, ruleFound, err = dr.matchLocalOverrides(name, target, r) status, ruleFound, err = dr.matchLocalOverrides(name, target, r)
if err != nil || ruleFound && status != chain.Allow { if err != nil || ruleFound && status != chain.Allow {
return return
} }
ruleFounds = append(ruleFounds, ruleFound) hasAllow = hasAllow || ruleFound
} }
status = chain.NoRuleFound if hasAllow {
for _, ruleFound = range ruleFounds { return chain.Allow, true, nil
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) { func (dr *defaultChainRouter) checkMorph(name chain.Name, rt RequestTarget, r resource.Request) (status chain.Status, ruleFound bool, err error) {
var ruleFounds []bool var hasAllow bool
for _, target := range rt.Targets() { for _, target := range rt.Targets() {
status, ruleFound, err = dr.matchMorphRuleChains(name, target, r) status, ruleFound, err = dr.matchMorphRuleChains(name, target, r)
if err != nil || ruleFound && status != chain.Allow { if err != nil || ruleFound && status != chain.Allow {
return return
} }
ruleFounds = append(ruleFounds, ruleFound) hasAllow = hasAllow || ruleFound
} }
status = chain.NoRuleFound if hasAllow {
for _, ruleFound = range ruleFounds { return chain.Allow, true, nil
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) { func (dr *defaultChainRouter) matchLocalOverrides(name chain.Name, target Target, r resource.Request) (status chain.Status, ruleFound bool, err error) {

View file

@ -1,14 +1,79 @@
package inmemory package inmemory
import ( import (
"bytes"
"testing" "testing"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
resourcetest "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil" resourcetest "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require" "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) { func TestInmemory(t *testing.T) {
const ( const (
object = "native::object::abc/xyz" object = "native::object::abc/xyz"
@ -48,13 +113,13 @@ func TestInmemory(t *testing.T) {
Condition: []chain.Condition{ Condition: []chain.Condition{
{ {
Op: chain.CondStringNotLike, Op: chain.CondStringNotLike,
Object: chain.ObjectRequest, Kind: chain.KindRequest,
Key: "SourceIP", Key: "SourceIP",
Value: "10.1.1.*", Value: "10.1.1.*",
}, },
{ {
Op: chain.CondStringNotEquals, Op: chain.CondStringNotEquals,
Object: chain.ObjectRequest, Kind: chain.KindRequest,
Key: "Actor", Key: "Actor",
Value: actor1, Value: actor1,
}, },
@ -63,6 +128,10 @@ func TestInmemory(t *testing.T) {
}, },
}) })
_, 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{ s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace2), &chain.Chain{
Rules: []chain.Rule{ Rules: []chain.Rule{
{ // Deny all expect "native::object::get" for all objects expect "native::object::abc/xyz". { // Deny all expect "native::object::get" for all objects expect "native::object::abc/xyz".
@ -73,6 +142,10 @@ 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{ s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(container), &chain.Chain{
Rules: []chain.Rule{ Rules: []chain.Rule{
{ // Allow to actor2 to get objects from the specific container only if they have `Department=HR` attribute. { // Allow to actor2 to get objects from the specific container only if they have `Department=HR` attribute.
@ -82,13 +155,13 @@ func TestInmemory(t *testing.T) {
Condition: []chain.Condition{ Condition: []chain.Condition{
{ {
Op: chain.CondStringEquals, Op: chain.CondStringEquals,
Object: chain.ObjectResource, Kind: chain.KindResource,
Key: "Department", Key: "Department",
Value: "HR", Value: "HR",
}, },
{ {
Op: chain.CondStringEquals, Op: chain.CondStringEquals,
Object: chain.ObjectRequest, Kind: chain.KindRequest,
Key: "Actor", Key: "Actor",
Value: actor2, Value: actor2,
}, },
@ -97,6 +170,14 @@ 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))
t.Run("bad subnet, namespace deny", func(t *testing.T) { t.Run("bad subnet, namespace deny", func(t *testing.T) {
// Request initiating from the untrusted subnet. // Request initiating from the untrusted subnet.
reqBadIP := resourcetest.NewRequest("native::object::put", res, map[string]string{ reqBadIP := resourcetest.NewRequest("native::object::put", res, map[string]string{
@ -175,6 +256,14 @@ 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) status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqGood)
require.Equal(t, chain.NoRuleFound, status) require.Equal(t, chain.NoRuleFound, status)
require.False(t, ok) require.False(t, ok)
@ -190,6 +279,14 @@ 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) status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqGood)
require.Equal(t, chain.QuotaLimitReached, status) require.Equal(t, chain.QuotaLimitReached, status)
require.True(t, ok) require.True(t, ok)
@ -198,9 +295,83 @@ func TestInmemory(t *testing.T) {
err := s.LocalStorage().RemoveOverride(chain.Ingress, engine.ContainerTarget(container), quotaRuleChainID) err := s.LocalStorage().RemoveOverride(chain.Ingress, engine.ContainerTarget(container), quotaRuleChainID)
require.NoError(t, err) 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) status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqGood)
require.Equal(t, chain.NoRuleFound, status) require.Equal(t, chain.NoRuleFound, status)
require.False(t, ok) 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
} }

View file

@ -52,6 +52,10 @@ func (s *inmemoryLocalStorage) AddOverride(name chain.Name, target engine.Target
s.guard.Lock() s.guard.Lock()
defer s.guard.Unlock() defer s.guard.Unlock()
if target.Name == "" {
target.Name = "root"
}
// AddOverride assigns generated chain ID if it has not been assigned. // AddOverride assigns generated chain ID if it has not been assigned.
if len(c.ID) == 0 { if len(c.ID) == 0 {
c.ID = s.generateChainID(name, target) c.ID = s.generateChainID(name, target)
@ -115,6 +119,24 @@ func (s *inmemoryLocalStorage) RemoveOverride(name chain.Name, target engine.Tar
return engine.ErrChainNotFound 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) { func (s *inmemoryLocalStorage) ListOverrides(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
s.guard.RLock() s.guard.RLock()
defer s.guard.RUnlock() defer s.guard.RUnlock()

View file

@ -110,6 +110,52 @@ func TestRemoveOverride(t *testing.T) {
require.True(t, ok) require.True(t, ok)
require.Len(t, resourceChains, 0) 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) { func TestGetOverride(t *testing.T) {

View file

@ -3,27 +3,26 @@ package inmemory
import ( import (
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
"github.com/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/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
) )
type inmemoryMorphRuleChainStorage struct { type inmemoryMorphRuleChainStorage struct {
nameToNamespaceChains engine.LocalOverrideStorage storage engine.LocalOverrideStorage
nameToContainerChains engine.LocalOverrideStorage
} }
func NewInmemoryMorphRuleChainStorage() engine.MorphRuleChainStorage { func NewInmemoryMorphRuleChainStorage() engine.MorphRuleChainStorage {
return &inmemoryMorphRuleChainStorage{ return &inmemoryMorphRuleChainStorage{
nameToNamespaceChains: NewInmemoryLocalStorage(), storage: NewInmemoryLocalStorage(),
nameToContainerChains: NewInmemoryLocalStorage(),
} }
} }
func (s *inmemoryMorphRuleChainStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (_ util.Uint256, _ uint32, err error) { func (s *inmemoryMorphRuleChainStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (_ util.Uint256, _ uint32, err error) {
switch target.Type { switch target.Type {
case engine.Namespace: case engine.Namespace, engine.Container, engine.User, engine.Group:
_, err = s.nameToNamespaceChains.AddOverride(name, target, c) _, err = s.storage.AddOverride(name, target, c)
case engine.Container:
_, err = s.nameToContainerChains.AddOverride(name, target, c)
default: default:
err = engine.ErrUnknownTarget err = engine.ErrUnknownTarget
} }
@ -32,10 +31,18 @@ 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) { func (s *inmemoryMorphRuleChainStorage) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (_ util.Uint256, _ uint32, err error) {
switch target.Type { switch target.Type {
case engine.Namespace: case engine.Namespace, engine.Container, engine.User, engine.Group:
err = s.nameToNamespaceChains.RemoveOverride(name, target, chainID) err = s.storage.RemoveOverride(name, target, chainID)
case engine.Container: default:
err = s.nameToContainerChains.RemoveOverride(name, target, chainID) 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)
default: default:
err = engine.ErrUnknownTarget err = engine.ErrUnknownTarget
} }
@ -44,10 +51,8 @@ func (s *inmemoryMorphRuleChainStorage) RemoveMorphRuleChain(name chain.Name, ta
func (s *inmemoryMorphRuleChainStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) { func (s *inmemoryMorphRuleChainStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
switch target.Type { switch target.Type {
case engine.Namespace: case engine.Namespace, engine.Container, engine.User, engine.Group:
return s.nameToNamespaceChains.ListOverrides(name, target) return s.storage.ListOverrides(name, target)
case engine.Container:
return s.nameToContainerChains.ListOverrides(name, target)
default: default:
} }
return nil, engine.ErrUnknownTarget return nil, engine.ErrUnknownTarget
@ -60,3 +65,48 @@ func (s *inmemoryMorphRuleChainStorage) GetAdmin() (util.Uint160, error) {
func (s *inmemoryMorphRuleChainStorage) SetAdmin(_ util.Uint160) (util.Uint256, uint32, error) { func (s *inmemoryMorphRuleChainStorage) SetAdmin(_ util.Uint160) (util.Uint256, uint32, error) {
panic("not implemented") 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
}

View file

@ -3,6 +3,8 @@ package engine
import ( import (
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" "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" "github.com/nspcc-dev/neo-go/pkg/util"
) )
@ -21,6 +23,8 @@ type LocalOverrideStorage interface {
RemoveOverride(name chain.Name, target Target, chainID chain.ID) error 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) ListOverrides(name chain.Name, target Target) ([]*chain.Chain, error)
DropAllOverrides(name chain.Name) error DropAllOverrides(name chain.Name) error
@ -33,6 +37,8 @@ type TargetType rune
const ( const (
Namespace TargetType = 'n' Namespace TargetType = 'n'
Container TargetType = 'c' Container TargetType = 'c'
User TargetType = 'u'
Group TargetType = 'g'
) )
type Target struct { type Target struct {
@ -44,6 +50,8 @@ type Target struct {
type RequestTarget struct { type RequestTarget struct {
Namespace *Target Namespace *Target
Container *Target Container *Target
User *Target
Groups []Target
} }
func NewRequestTargetWithNamespace(namespace string) RequestTarget { func NewRequestTargetWithNamespace(namespace string) RequestTarget {
@ -69,6 +77,24 @@ 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) { func (rt *RequestTarget) Targets() (targets []Target) {
if rt.Namespace != nil { if rt.Namespace != nil {
targets = append(targets, *rt.Namespace) targets = append(targets, *rt.Namespace)
@ -76,6 +102,12 @@ func (rt *RequestTarget) Targets() (targets []Target) {
if rt.Container != nil { if rt.Container != nil {
targets = append(targets, *rt.Container) targets = append(targets, *rt.Container)
} }
if rt.User != nil {
targets = append(targets, *rt.User)
}
if len(rt.Groups) != 0 {
targets = append(targets, rt.Groups...)
}
return return
} }
@ -93,6 +125,20 @@ 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 // MorphRuleChainStorageReader is the interface that provides read-only methods to receive
// data like chains, target or admin from a chain storage. // data like chains, target or admin from a chain storage.
type MorphRuleChainStorageReader interface { type MorphRuleChainStorageReader interface {
@ -100,6 +146,9 @@ type MorphRuleChainStorageReader interface {
ListMorphRuleChains(name chain.Name, target Target) ([]*chain.Chain, error) ListMorphRuleChains(name chain.Name, target Target) ([]*chain.Chain, error)
GetAdmin() (util.Uint160, 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 read and manage data within a chain storage.
@ -113,6 +162,9 @@ type MorphRuleChainStorage interface {
// RemoveMorphRuleChain removes a chain rule to the policy contract and returns transaction hash, VUB and error. // RemoveMorphRuleChain removes a chain rule to the policy contract and returns transaction hash, VUB and error.
RemoveMorphRuleChain(name chain.Name, target Target, chainID chain.ID) (util.Uint256, uint32, error) RemoveMorphRuleChain(name chain.Name, target Target, chainID chain.ID) (util.Uint256, uint32, error)
// 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) SetAdmin(addr util.Uint160) (util.Uint256, uint32, error)
} }

View file

@ -6,11 +6,15 @@ import (
"math/big" "math/big"
"strings" "strings"
"git.frostfs.info/TrueCloudLab/frostfs-contract/commonclient"
"git.frostfs.info/TrueCloudLab/frostfs-contract/policy" "git.frostfs.info/TrueCloudLab/frostfs-contract/policy"
client "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/policy" client "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/policy"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "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/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neo-go/pkg/wallet"
@ -24,6 +28,10 @@ var (
// ContractStorage is the interface to manage chain rules within Policy contract. // ContractStorage is the interface to manage chain rules within Policy contract.
type ContractStorage struct { type ContractStorage struct {
hash util.Uint160
actor ContractStorageActor
contractInterface *client.Contract contractInterface *client.Contract
} }
@ -31,23 +39,49 @@ var _ engine.MorphRuleChainStorage = (*ContractStorage)(nil)
// ContractStorageReader is the interface to read data from Policy contract. // ContractStorageReader is the interface to read data from Policy contract.
type ContractStorageReader struct { type ContractStorageReader struct {
hash util.Uint160
invoker ContractStorageInvoker
contractReaderInterface *client.ContractReader contractReaderInterface *client.ContractReader
} }
type ContractStorageActor interface {
client.Actor
GetRPCInvoker() neoinvoker.RPCInvoke
}
var _ engine.MorphRuleChainStorageReader = (*ContractStorageReader)(nil) var _ engine.MorphRuleChainStorageReader = (*ContractStorageReader)(nil)
func NewContractStorage(actor client.Actor, contract util.Uint160) *ContractStorage { func NewContractStorage(actor ContractStorageActor, contract util.Uint160) *ContractStorage {
return &ContractStorage{ return &ContractStorage{
hash: contract,
actor: actor,
contractInterface: client.New(actor, contract), 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) { func NewContractStorageWithSimpleActor(rpcActor actor.RPCActor, acc *wallet.Account, contract util.Uint160) (*ContractStorage, error) {
act, err := actor.NewSimple(rpcActor, acc) act, err := actor.NewSimple(rpcActor, acc)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create simple actor: %w", err) return nil, fmt.Errorf("failed to create simple actor: %w", err)
} }
return NewContractStorage(act, contract), nil return NewContractStorage(&contractStorageActorImpl{Actor: act, rpcActor: rpcActor}, contract), nil
} }
func (s *ContractStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (txHash util.Uint256, vub uint32, err error) { func (s *ContractStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (txHash util.Uint256, vub uint32, err error) {
@ -84,17 +118,38 @@ func (s *ContractStorage) RemoveMorphRuleChain(name chain.Name, target engine.Ta
return return
} }
func (s *ContractStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) { 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) {
kind, err := policyKind(target.Type) kind, err := policyKind(target.Type)
if err != nil { if err != nil {
return nil, err return nil, err
} }
items, err := s.contractInterface.ListChainsByPrefix(big.NewInt(int64(kind)), target.Name, []byte(name)) const (
if err != nil { method = "iteratorChainsByPrefix"
return nil, err 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...)
if err != nil {
return nil, fmt.Errorf("read items error: %w", err)
}
var chains []*chain.Chain var chains []*chain.Chain
for _, item := range items { for _, item := range items {
serialized, err := bytesFromStackItem(item) serialized, err := bytesFromStackItem(item)
@ -107,10 +162,21 @@ func (s *ContractStorage) ListMorphRuleChains(name chain.Name, target engine.Tar
} }
chains = append(chains, c) chains = append(chains, c)
} }
return chains, nil 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) { func (s *ContractStorage) GetAdmin() (util.Uint160, error) {
return s.contractInterface.GetAdmin() return s.contractInterface.GetAdmin()
} }
@ -119,43 +185,35 @@ func (s *ContractStorage) SetAdmin(addr util.Uint160) (util.Uint256, uint32, err
return s.contractInterface.SetAdmin(addr) return s.contractInterface.SetAdmin(addr)
} }
func NewContractStorageReader(inv client.Invoker, contract util.Uint160) *ContractStorageReader { type ContractStorageInvoker interface {
client.Invoker
GetRPCInvoker() neoinvoker.RPCInvoke
}
func NewContractStorageReader(inv ContractStorageInvoker, contract util.Uint160) *ContractStorageReader {
return &ContractStorageReader{ return &ContractStorageReader{
hash: contract,
invoker: inv,
contractReaderInterface: client.NewReader(inv, contract), contractReaderInterface: client.NewReader(inv, contract),
} }
} }
func (s *ContractStorageReader) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) { func (s *ContractStorageReader) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
kind, err := policyKind(target.Type) return listChains(name, target, s.invoker.GetRPCInvoker(), s.hash)
if err != nil {
return nil, err
}
items, err := s.contractReaderInterface.ListChainsByPrefix(big.NewInt(int64(kind)), target.Name, []byte(name))
if err != nil {
return nil, err
}
var chains []*chain.Chain
for _, item := range items {
serialized, err := bytesFromStackItem(item)
if err != nil {
return nil, err
}
c := new(chain.Chain)
if err := c.DecodeBytes(serialized); err != nil {
return nil, err
}
chains = append(chains, c)
}
return chains, nil
} }
func (s *ContractStorageReader) GetAdmin() (util.Uint160, error) { func (s *ContractStorageReader) GetAdmin() (util.Uint160, error) {
return s.contractReaderInterface.GetAdmin() 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) { func bytesFromStackItem(param stackitem.Item) ([]byte, error) {
switch param.Type() { switch param.Type() {
case stackitem.BufferT, stackitem.ByteArrayT, stackitem.IntegerT: case stackitem.BufferT, stackitem.ByteArrayT, stackitem.IntegerT:
@ -175,10 +233,16 @@ func prefixedChainName(name chain.Name, chainID chain.ID) []byte {
} }
func policyKind(typ engine.TargetType) (policy.Kind, error) { func policyKind(typ engine.TargetType) (policy.Kind, error) {
if typ == engine.Namespace { switch typ {
case engine.Namespace:
return policy.Namespace, nil return policy.Namespace, nil
} else if typ == engine.Container { case engine.Container:
return policy.Container, nil 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
} }
}

View file

@ -2,4 +2,10 @@ package common
const ( const (
PropertyKeyFrostFSIDGroupID = "frostfsid:groupID" PropertyKeyFrostFSIDGroupID = "frostfsid:groupID"
PropertyKeyFrostFSSourceIP = "frostfs:sourceIP"
PropertyKeyFormatFrostFSIDUserClaim = "frostfsid:userClaim/%s"
PropertyKeyFrostFSXHeader = "frostfs:xheader/%s"
) )

View file

@ -8,6 +8,7 @@ const (
MethodSearchObject = "SearchObject" MethodSearchObject = "SearchObject"
MethodRangeObject = "RangeObject" MethodRangeObject = "RangeObject"
MethodHashObject = "HashObject" MethodHashObject = "HashObject"
MethodPatchObject = "PatchObject"
MethodPutContainer = "PutContainer" MethodPutContainer = "PutContainer"
MethodDeleteContainer = "DeleteContainer" MethodDeleteContainer = "DeleteContainer"

View file

@ -6,6 +6,13 @@ const (
PropertyKeyDelimiter = "s3:delimiter" PropertyKeyDelimiter = "s3:delimiter"
PropertyKeyPrefix = "s3:prefix" PropertyKeyPrefix = "s3:prefix"
PropertyKeyVersionID = "s3:VersionId" 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:::*" ResourceFormatS3All = "arn:aws:s3:::*"
ResourceFormatS3Bucket = "arn:aws:s3:::%s" ResourceFormatS3Bucket = "arn:aws:s3:::%s"