forked from TrueCloudLab/policy-engine
Compare commits
33 commits
feature/re
...
master
Author | SHA1 | Date | |
---|---|---|---|
a3bc3099bd | |||
ed14db3e66 | |||
eb7be61798 | |||
a1386f6d25 | |||
2300995af2 | |||
a11e80e2c7 | |||
|
96225afacb | ||
|
2628f61849 | ||
ac965e8d17 | |||
64e06f5b7c | |||
303a81cdc6 | |||
|
d7ed188f68 | ||
|
1f6f4163d4 | ||
|
84c4872b20 | ||
|
e75200bb8e | ||
2e7518c453 | |||
2fa27b6557 | |||
38f947ac0a | |||
34c1eafa56 | |||
84c15a559c | |||
|
c539728641 | ||
04a79f57ef | |||
ff5d05ac92 | |||
0e69e48511 | |||
530248de75 | |||
b6a6816800 | |||
1f190e1668 | |||
67cf09f51d | |||
84c6be01de | |||
67e4595a91 | |||
42497ad242 | |||
1d51f2121d | |||
9040e48504 |
38 changed files with 3180 additions and 552 deletions
|
@ -13,7 +13,7 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Run commit format checker
|
||||
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v3
|
||||
|
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: '1.23'
|
||||
cache: true
|
||||
|
||||
- name: Install linters
|
||||
|
@ -25,7 +25,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go_versions: [ '1.20', '1.21' ]
|
||||
go_versions: [ '1.22', '1.23' ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
@ -48,7 +48,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: '1.23'
|
||||
cache: true
|
||||
|
||||
- name: Run tests
|
||||
|
@ -63,7 +63,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: '1.23'
|
||||
cache: true
|
||||
|
||||
- name: Install staticcheck
|
||||
|
|
|
@ -12,7 +12,8 @@ run:
|
|||
# output configuration options
|
||||
output:
|
||||
# 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
|
||||
linters-settings:
|
||||
|
|
|
@ -2,13 +2,6 @@ ci:
|
|||
autofix_prs: false
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/jorisroovers/gitlint
|
||||
rev: v0.19.1
|
||||
hooks:
|
||||
- id: gitlint
|
||||
stages: [commit-msg]
|
||||
- id: gitlint-ci
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
|
@ -42,7 +35,7 @@ repos:
|
|||
hooks:
|
||||
- id: go-unit-tests
|
||||
name: go unit tests
|
||||
entry: make test
|
||||
entry: make test GOFLAGS=''
|
||||
pass_filenames: false
|
||||
types: [go]
|
||||
language: system
|
||||
|
|
7
Makefile
7
Makefile
|
@ -1,9 +1,9 @@
|
|||
#!/usr/bin/make -f
|
||||
|
||||
TRUECLOUDLAB_LINT_VERSION ?= 0.0.2
|
||||
TRUECLOUDLAB_LINT_VERSION ?= 0.0.6
|
||||
TMP_DIR := .cache
|
||||
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)
|
||||
EASYJSON_VERSION ?= $(shell go list -f '{{.Version}}' -m github.com/mailru/easyjson)
|
||||
EASYJSON_DIR ?= $(shell pwd)/bin/easyjson-$(EASYJSON_VERSION)
|
||||
|
@ -22,9 +22,10 @@ imports:
|
|||
@goimports -w .
|
||||
|
||||
# Run Unit Test with go test
|
||||
test: GOFLAGS ?= "-count=1"
|
||||
test:
|
||||
@echo "⇒ Running go test"
|
||||
@go test ./... -count=1
|
||||
@GOFLAGS="$(GOFLAGS)" go test ./...
|
||||
|
||||
# Activate pre-commit hooks
|
||||
pre-commit:
|
||||
|
|
85
docs/ape.md
Normal file
85
docs/ape.md
Normal 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)
|
61
docs/images/ape/s3_ape.puml
Normal file
61
docs/images/ape/s3_ape.puml
Normal 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
|
73
docs/images/ape/s3_ape.svg
Normal file
73
docs/images/ape/s3_ape.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 22 KiB |
46
docs/images/ape/storage_node_ape.puml
Normal file
46
docs/images/ape/storage_node_ape.puml
Normal 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
|
58
docs/images/ape/storage_node_ape.svg
Normal file
58
docs/images/ape/storage_node_ape.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 17 KiB |
439
docs/policy_converters.md
Normal file
439
docs/policy_converters.md
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
4
go.mod
4
go.mod
|
@ -1,9 +1,9 @@
|
|||
module git.frostfs.info/TrueCloudLab/policy-engine
|
||||
|
||||
go 1.20
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.0
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409111539-e7a05a49ff45
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/mailru/easyjson v0.7.7
|
||||
github.com/nspcc-dev/neo-go v0.105.0
|
||||
|
|
29
go.sum
29
go.sum
|
@ -1,11 +1,17 @@
|
|||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.0 h1:FzurjElUwC7InY9v5rzXReKbfBL5yRJKSWJPq6BKhH0=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.0/go.mod h1:F/fe1OoIDKr5Bz99q4sriuHDuf3aZefZy9ZsCqEtgxc=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409111539-e7a05a49ff45 h1:Tp4I+XOLp3VCJORfxSamQtj3RZNISbaLM4WD5iIzXxg=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409111539-e7a05a49ff45/go.mod h1:F/fe1OoIDKr5Bz99q4sriuHDuf3aZefZy9ZsCqEtgxc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c=
|
||||
github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ=
|
||||
github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI=
|
||||
github.com/consensys/gnark-crypto v0.12.2-0.20231013160410-1f65e75b6dfb h1:f0BMgIjhZy4lSRHCXFbQst85f5agZAjtDMixQqBWNpc=
|
||||
github.com/consensys/gnark-crypto v0.12.2-0.20231013160410-1f65e75b6dfb/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
|
@ -22,6 +28,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
|
|||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
|
@ -30,16 +37,20 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
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/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
|
||||
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM=
|
||||
github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
|
||||
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20231123160306-3374ff1e7a3c h1:OOQeE613BH93ICPq3eke5N78gWNeMjcBWkmD2NKyXVg=
|
||||
|
@ -62,11 +73,17 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
|
|||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
|
||||
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
|
||||
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
|
||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs=
|
||||
|
@ -74,11 +91,13 @@ github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954/go.mod h1:u2MKk
|
|||
github.com/twmb/murmur3 v1.1.5 h1:i9OLS9fkuLzBXjt6dptlAEyk58fJsSTXbRg3SgVyqgk=
|
||||
github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
|
||||
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
|
||||
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo=
|
||||
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c=
|
||||
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
|
||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
|
@ -90,11 +109,13 @@ golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq
|
|||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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=
|
||||
|
@ -111,6 +132,7 @@ golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
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=
|
||||
|
@ -118,6 +140,7 @@ 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=
|
||||
|
@ -128,6 +151,7 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
|
|||
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
|
@ -139,3 +163,4 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
|
||||
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=
|
||||
|
|
150
iam/converter.go
150
iam/converter.go
|
@ -3,15 +3,77 @@ package iam
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/schema/common"
|
||||
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
||||
)
|
||||
|
||||
const 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 (
|
||||
// String condition operators.
|
||||
|
@ -122,10 +184,10 @@ func convertToChainCondition(c Conditions) ([]GroupedConditions, error) {
|
|||
}
|
||||
|
||||
group.Conditions[i] = chain.Condition{
|
||||
Op: condType,
|
||||
Object: chain.ObjectRequest,
|
||||
Key: key,
|
||||
Value: converted,
|
||||
Op: condType,
|
||||
Kind: chain.KindRequest,
|
||||
Key: transformKey(key),
|
||||
Value: converted,
|
||||
}
|
||||
}
|
||||
grouped = append(grouped, group)
|
||||
|
@ -135,6 +197,20 @@ func convertToChainCondition(c Conditions) ([]GroupedConditions, error) {
|
|||
return grouped, nil
|
||||
}
|
||||
|
||||
func transformKey(key string) string {
|
||||
tagName, isTag := strings.CutPrefix(key, condKeyAWSPrincipalTagPrefix)
|
||||
if isTag {
|
||||
return fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, userClaimTagPrefix+tagName)
|
||||
}
|
||||
|
||||
switch key {
|
||||
case condKeyAWSSourceIP:
|
||||
return common.PropertyKeyFrostFSSourceIP
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFunction, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(op, "String"):
|
||||
|
@ -168,8 +244,7 @@ func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFuncti
|
|||
return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op)
|
||||
}
|
||||
case strings.HasPrefix(op, "Numeric"):
|
||||
// TODO
|
||||
return 0, nil, fmt.Errorf("currently nummeric conditions unsupported: '%s'", op)
|
||||
return numericConditionTypeAndConverter(op)
|
||||
case strings.HasPrefix(op, "Date"):
|
||||
switch op {
|
||||
case CondDateEquals:
|
||||
|
@ -190,13 +265,9 @@ func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFuncti
|
|||
case op == CondBool:
|
||||
return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil
|
||||
case op == CondIPAddress:
|
||||
// todo consider using converters
|
||||
// "203.0.113.0/24" -> "203.0.113.*",
|
||||
// "2001:DB8:1234:5678::/64" -> "2001:DB8:1234:5678:*"
|
||||
// or having specific condition type for IP
|
||||
return chain.CondStringLike, noConvertFunction, nil
|
||||
return chain.CondIPAddress, ipConvertFunction, nil
|
||||
case op == CondNotIPAddress:
|
||||
return chain.CondStringNotLike, noConvertFunction, nil
|
||||
return chain.CondNotIPAddress, ipConvertFunction, nil
|
||||
case op == CondSliceContains:
|
||||
return chain.CondSliceContains, noConvertFunction, nil
|
||||
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)
|
||||
|
||||
func noConvertFunction(val string) (string, error) {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func numericConvertFunction(val string) (string, error) {
|
||||
if _, err := fixedn.Fixed8FromString(val); err == nil {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("invalid numeric value: '%s'", val)
|
||||
}
|
||||
|
||||
func ipConvertFunction(val string) (string, error) {
|
||||
if _, err := netip.ParsePrefix(val); err != nil {
|
||||
if _, err = netip.ParseAddr(val); err != nil {
|
||||
return "", err
|
||||
}
|
||||
val += "/32"
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func dateConvertFunction(val string) (string, error) {
|
||||
if _, err := strconv.ParseInt(val, 10, 64); err == nil {
|
||||
return val, nil
|
||||
|
@ -269,21 +378,18 @@ func validateResource(resource string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func validateAction(action string) error {
|
||||
if action == Wildcard {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(action, s3ActionPrefix) && !strings.HasPrefix(action, iamActionPrefix) {
|
||||
return ErrInvalidActionFormat
|
||||
func validateAction(action string) (bool, error) {
|
||||
isIAM := strings.HasPrefix(action, iamActionPrefix)
|
||||
if !strings.HasPrefix(action, s3ActionPrefix) && !isIAM {
|
||||
return false, ErrInvalidActionFormat
|
||||
}
|
||||
|
||||
index := strings.IndexByte(action, Wildcard[0])
|
||||
if index != -1 && index != utf8.RuneCountInString(action)-1 {
|
||||
return ErrInvalidActionFormat
|
||||
return false, ErrInvalidActionFormat
|
||||
}
|
||||
|
||||
return nil
|
||||
return isIAM, nil
|
||||
}
|
||||
|
||||
func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package iam
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
|
@ -10,18 +11,56 @@ import (
|
|||
|
||||
const PropertyKeyFilePath = "FilePath"
|
||||
|
||||
var supportedActionToNativeOpMap = map[string][]string{
|
||||
supportedS3NativeActionDeleteObject: {native.MethodDeleteObject, native.MethodHeadObject},
|
||||
supportedS3NativeActionHeadObject: {native.MethodHeadObject},
|
||||
supportedS3NativeActionGetObject: {native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
||||
supportedS3NativeActionPutObject: {native.MethodPutObject},
|
||||
supportedS3NativeActionListBucket: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
||||
|
||||
supportedS3NativeActionCreateBucket: {native.MethodPutContainer},
|
||||
supportedS3NativeActionDeleteBucket: {native.MethodDeleteContainer},
|
||||
supportedS3NativeActionListAllMyBucket: {native.MethodListContainers},
|
||||
supportedS3NativeActionPutBucketACL: {native.MethodSetContainerEACL},
|
||||
supportedS3NativeActionGetBucketACL: {native.MethodGetContainerEACL},
|
||||
var actionToNativeOpMap = map[string][]string{
|
||||
s3ActionAbortMultipartUpload: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
|
||||
s3ActionCreateBucket: {native.MethodGetContainer, native.MethodPutContainer, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodPutObject},
|
||||
s3ActionDeleteBucket: {native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject, native.MethodGetObject},
|
||||
s3ActionDeleteBucketPolicy: {native.MethodGetContainer},
|
||||
s3ActionDeleteObject: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject},
|
||||
s3ActionDeleteObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
|
||||
s3ActionDeleteObjectVersion: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject},
|
||||
s3ActionDeleteObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
|
||||
s3ActionGetBucketACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject},
|
||||
s3ActionGetBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
|
||||
s3ActionGetBucketLocation: {native.MethodGetContainer},
|
||||
s3ActionGetBucketNotification: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
|
||||
s3ActionGetBucketObjectLockConfiguration: {native.MethodGetContainer, native.MethodGetObject},
|
||||
s3ActionGetBucketPolicy: {native.MethodGetContainer},
|
||||
s3ActionGetBucketPolicyStatus: {native.MethodGetContainer},
|
||||
s3ActionGetBucketTagging: {native.MethodGetContainer, native.MethodGetObject},
|
||||
s3ActionGetBucketVersioning: {native.MethodGetContainer, native.MethodGetObject},
|
||||
s3ActionGetLifecycleConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
|
||||
s3ActionGetObject: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
||||
s3ActionGetObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
|
||||
s3ActionGetObjectAttributes: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
|
||||
s3ActionGetObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject},
|
||||
s3ActionGetObjectRetention: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject},
|
||||
s3ActionGetObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject},
|
||||
s3ActionGetObjectVersion: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
||||
s3ActionGetObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
|
||||
s3ActionGetObjectVersionAttributes: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
|
||||
s3ActionGetObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject},
|
||||
s3ActionListAllMyBuckets: {native.MethodListContainers, native.MethodGetContainer},
|
||||
s3ActionListBucket: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
||||
s3ActionListBucketMultipartUploads: {native.MethodGetContainer, native.MethodGetObject},
|
||||
s3ActionListBucketVersions: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
||||
s3ActionListMultipartUploadParts: {native.MethodGetContainer, native.MethodGetObject},
|
||||
s3ActionPutBucketACL: {native.MethodGetContainer, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodPutObject},
|
||||
s3ActionPutBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject},
|
||||
s3ActionPutBucketNotification: {native.MethodGetContainer, native.MethodHeadObject, native.MethodDeleteObject, native.MethodGetObject, native.MethodPutObject},
|
||||
s3ActionPutBucketObjectLockConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject},
|
||||
s3ActionPutBucketPolicy: {native.MethodGetContainer},
|
||||
s3ActionPutBucketTagging: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject},
|
||||
s3ActionPutBucketVersioning: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject},
|
||||
s3ActionPutLifecycleConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodPutObject, native.MethodDeleteObject},
|
||||
s3ActionPutObject: {native.MethodGetContainer, native.MethodPutObject, native.MethodGetObject, native.MethodHeadObject, native.MethodRangeObject},
|
||||
s3ActionPutObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
|
||||
s3ActionPutObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
|
||||
s3ActionPutObjectRetention: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
|
||||
s3ActionPutObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
|
||||
s3ActionPutObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
|
||||
s3ActionPutObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
|
||||
s3ActionPatchObject: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodPatchObject, native.MethodPutObject, native.MethodRangeObject},
|
||||
}
|
||||
|
||||
var containerNativeOperations = map[string]struct{}{
|
||||
|
@ -43,19 +82,7 @@ var objectNativeOperations = map[string]struct{}{
|
|||
native.MethodHashObject: {},
|
||||
}
|
||||
|
||||
const (
|
||||
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"
|
||||
)
|
||||
var errConditionKeyNotApplicable = errors.New("condition key is not applicable")
|
||||
|
||||
type NativeResolver interface {
|
||||
GetUserKey(account, name string) (string, error)
|
||||
|
@ -76,6 +103,11 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro
|
|||
|
||||
for _, statement := range p.Statement {
|
||||
status := formStatus(statement)
|
||||
if status != chain.Allow {
|
||||
// Most s3 methods share the same native operations. Deny rules must not affect shared native operations,
|
||||
// therefore this code skips all deny rules for native protocol. Deny is applied for s3 protocol only, in this case.
|
||||
continue
|
||||
}
|
||||
|
||||
action, actionInverted := statement.action()
|
||||
nativeActions, err := formNativeActionNames(action)
|
||||
|
@ -95,6 +127,9 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro
|
|||
|
||||
groupedConditions, err := convertToNativeChainCondition(statement.Conditions, resolver)
|
||||
if err != nil {
|
||||
if errors.Is(err, errConditionKeyNotApplicable) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
splitConditions := splitGroupedConditions(groupedConditions)
|
||||
|
@ -146,8 +181,8 @@ func getActionTypes(nativeActions []string) ActionTypes {
|
|||
_, isObj := objectNativeOperations[action]
|
||||
_, isCnr := containerNativeOperations[action]
|
||||
|
||||
res.Object = isObj || action == Wildcard
|
||||
res.Container = isCnr || action == Wildcard
|
||||
res.Object = res.Object || isObj || action == Wildcard
|
||||
res.Container = res.Container || isCnr || action == Wildcard
|
||||
}
|
||||
|
||||
return res
|
||||
|
@ -180,28 +215,43 @@ func getNativePrincipalsAndConditionFunc(statement Statement, resolver NativeRes
|
|||
|
||||
return principals, func(principal string) chain.Condition {
|
||||
return chain.Condition{
|
||||
Op: op,
|
||||
Object: chain.ObjectRequest,
|
||||
Key: native.PropertyKeyActorPublicKey,
|
||||
Value: principal,
|
||||
Op: op,
|
||||
Kind: chain.KindRequest,
|
||||
Key: native.PropertyKeyActorPublicKey,
|
||||
Value: principal,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertToNativeChainCondition(c Conditions, resolver NativeResolver) ([]GroupedConditions, error) {
|
||||
return convertToChainConditions(c, func(gr GroupedConditions) (GroupedConditions, error) {
|
||||
res := GroupedConditions{
|
||||
Conditions: make([]chain.Condition, 0, len(gr.Conditions)),
|
||||
Any: gr.Any,
|
||||
}
|
||||
|
||||
for i := range gr.Conditions {
|
||||
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
|
||||
val, err := formPrincipalKey(gr.Conditions[i].Value, resolver)
|
||||
if err != nil {
|
||||
return GroupedConditions{}, err
|
||||
}
|
||||
gr.Conditions[i].Value = val
|
||||
res.Conditions = append(res.Conditions, gr.Conditions[i])
|
||||
case strings.HasPrefix(gr.Conditions[i].Key, condKeyAWSRequestTagPrefix) ||
|
||||
strings.HasPrefix(gr.Conditions[i].Key, condKeyAWSResourceTagPrefix):
|
||||
// Tags exist only in S3 requests, so native protocol should not process such conditions.
|
||||
continue
|
||||
default:
|
||||
res.Conditions = append(res.Conditions, gr.Conditions[i])
|
||||
}
|
||||
}
|
||||
|
||||
return gr, nil
|
||||
return res, nil
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -222,7 +272,7 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve
|
|||
|
||||
res := make([]GroupedResources, 0, len(names))
|
||||
|
||||
var combined []string
|
||||
combined := make(map[string]struct{})
|
||||
|
||||
for _, resource := range names {
|
||||
if err := validateResource(resource); err != nil {
|
||||
|
@ -261,29 +311,38 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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{
|
||||
{
|
||||
Op: chain.CondStringLike,
|
||||
Object: chain.ObjectResource,
|
||||
Key: PropertyKeyFilePath,
|
||||
Value: obj,
|
||||
Op: chain.CondStringLike,
|
||||
Kind: chain.KindResource,
|
||||
Key: PropertyKeyFilePath,
|
||||
Value: obj,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -329,26 +388,39 @@ func formPrincipalKey(principal string, resolver NativeResolver) (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 {
|
||||
if err := validateAction(action); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if action == Wildcard {
|
||||
return []string{Wildcard}, nil
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(action, s3ActionPrefix) {
|
||||
isIAM, err := validateAction(action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isIAM {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.TrimPrefix(action, s3ActionPrefix) == Wildcard {
|
||||
if action[len(s3ActionPrefix):] == Wildcard {
|
||||
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
|
||||
|
|
|
@ -2,81 +2,71 @@ package iam
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
|
||||
)
|
||||
|
||||
var specialActionToS3OpMap = map[string][]string{
|
||||
specialS3ActionsListAllMyBuckets: {"s3:ListBuckets"},
|
||||
specialS3ActionsListBucket: {"s3:HeadBucket", "s3:GetBucketLocation", "s3:ListObjectsV1", "s3:ListObjectsV2"},
|
||||
specialS3ActionsListBucketVersions: {"s3:ListBucketObjectVersions"},
|
||||
specialS3ActionsListBucketMultipartUploads: {"s3:ListMultipartUploads"},
|
||||
specialS3ActionsGetBucketObjectLockConfiguration: {"s3:GetBucketObjectLockConfig"},
|
||||
specialS3ActionsGetEncryptionConfiguration: {"s3:GetBucketEncryption"},
|
||||
specialS3ActionsGetLifecycleConfiguration: {"s3:GetBucketLifecycle"},
|
||||
specialS3ActionsGetBucketACL: {"s3:GetBucketACL"},
|
||||
specialS3ActionsGetBucketCORS: {"s3:GetBucketCors"},
|
||||
specialS3ActionsPutBucketTagging: {"s3:PutBucketTagging", "s3:DeleteBucketTagging"},
|
||||
specialS3ActionsPutBucketObjectLockConfiguration: {"s3:PutBucketObjectLockConfig"},
|
||||
specialS3ActionsPutEncryptionConfiguration: {"s3:PutBucketEncryption", "s3:DeleteBucketEncryption"},
|
||||
specialS3ActionsPutLifecycleConfiguration: {"s3:PutBucketLifecycle", "s3:DeleteBucketLifecycle"},
|
||||
specialS3ActionsPutBucketACL: {"s3:PutBucketACL"},
|
||||
specialS3ActionsPutBucketCORS: {"s3:PutBucketCors", "s3:DeleteBucketCors"},
|
||||
specialS3ActionsDeleteBucketCORS: {"s3:DeleteBucketCors"},
|
||||
const condKeyAWSMFAPresent = "aws:MultiFactorAuthPresent"
|
||||
|
||||
specialS3ActionsListMultipartUploadParts: {"s3:ListParts"},
|
||||
specialS3ActionsGetObjectACL: {"s3:GetObjectACL"},
|
||||
specialS3ActionsGetObject: {"s3:GetObject", "s3:HeadObject"},
|
||||
specialS3ActionsGetObjectVersion: {"s3:GetObject", "s3:HeadObject"},
|
||||
specialS3ActionsGetObjectVersionACL: {"s3:GetObjectACL"},
|
||||
specialS3ActionsGetObjectVersionAttributes: {"s3:GetObjectAttributes"},
|
||||
specialS3ActionsGetObjectVersionTagging: {"s3:GetObjectTagging"},
|
||||
specialS3ActionsPutObjectACL: {"s3:PutObjectACL"},
|
||||
specialS3ActionsPutObjectVersionACL: {"s3:PutObjectACL"},
|
||||
specialS3ActionsPutObjectVersionTagging: {"s3:PutObjectTagging"},
|
||||
specialS3ActionsPutObject: {
|
||||
var actionToS3OpMap = map[string][]string{
|
||||
s3ActionAbortMultipartUpload: {s3ActionAbortMultipartUpload},
|
||||
s3ActionCreateBucket: {s3ActionCreateBucket},
|
||||
s3ActionDeleteBucket: {s3ActionDeleteBucket},
|
||||
s3ActionDeleteBucketPolicy: {s3ActionDeleteBucketPolicy},
|
||||
s3ActionDeleteObjectTagging: {s3ActionDeleteObjectTagging},
|
||||
s3ActionGetBucketLocation: {s3ActionGetBucketLocation},
|
||||
s3ActionGetBucketNotification: {s3ActionGetBucketNotification},
|
||||
s3ActionGetBucketPolicy: {s3ActionGetBucketPolicy},
|
||||
s3ActionGetBucketPolicyStatus: {s3ActionGetBucketPolicyStatus},
|
||||
s3ActionGetBucketTagging: {s3ActionGetBucketTagging},
|
||||
s3ActionGetBucketVersioning: {s3ActionGetBucketVersioning},
|
||||
s3ActionGetObjectAttributes: {s3ActionGetObjectAttributes},
|
||||
s3ActionGetObjectLegalHold: {s3ActionGetObjectLegalHold},
|
||||
s3ActionGetObjectRetention: {s3ActionGetObjectRetention},
|
||||
s3ActionGetObjectTagging: {s3ActionGetObjectTagging},
|
||||
s3ActionPutBucketNotification: {s3ActionPutBucketNotification},
|
||||
s3ActionPutBucketPolicy: {s3ActionPutBucketPolicy},
|
||||
s3ActionPutBucketVersioning: {s3ActionPutBucketVersioning},
|
||||
s3ActionPutObjectLegalHold: {s3ActionPutObjectLegalHold},
|
||||
s3ActionPutObjectRetention: {s3ActionPutObjectRetention},
|
||||
s3ActionPutObjectTagging: {s3ActionPutObjectTagging},
|
||||
s3ActionPatchObject: {s3ActionPatchObject},
|
||||
|
||||
s3ActionListAllMyBuckets: {"s3:ListBuckets"},
|
||||
s3ActionListBucket: {"s3:HeadBucket", "s3:GetBucketLocation", "s3:ListObjectsV1", "s3:ListObjectsV2"},
|
||||
s3ActionListBucketVersions: {"s3:ListBucketObjectVersions"},
|
||||
s3ActionListBucketMultipartUploads: {"s3:ListMultipartUploads"},
|
||||
s3ActionGetBucketObjectLockConfiguration: {"s3:GetBucketObjectLockConfig"},
|
||||
s3ActionGetLifecycleConfiguration: {"s3:GetBucketLifecycle"},
|
||||
s3ActionGetBucketACL: {"s3:GetBucketACL"},
|
||||
s3ActionGetBucketCORS: {"s3:GetBucketCors"},
|
||||
s3ActionPutBucketTagging: {"s3:PutBucketTagging", "s3:DeleteBucketTagging"},
|
||||
s3ActionPutBucketObjectLockConfiguration: {"s3:PutBucketObjectLockConfig"},
|
||||
s3ActionPutLifecycleConfiguration: {"s3:PutBucketLifecycle", "s3:DeleteBucketLifecycle"},
|
||||
s3ActionPutBucketACL: {"s3:PutBucketACL"},
|
||||
s3ActionPutBucketCORS: {"s3:PutBucketCors", "s3:DeleteBucketCors"},
|
||||
|
||||
s3ActionListMultipartUploadParts: {"s3:ListParts"},
|
||||
s3ActionGetObjectACL: {"s3:GetObjectACL"},
|
||||
s3ActionGetObject: {"s3:GetObject", "s3:HeadObject"},
|
||||
s3ActionGetObjectVersion: {"s3:GetObject", "s3:HeadObject"},
|
||||
s3ActionGetObjectVersionACL: {"s3:GetObjectACL"},
|
||||
s3ActionGetObjectVersionAttributes: {"s3:GetObjectAttributes"},
|
||||
s3ActionGetObjectVersionTagging: {"s3:GetObjectTagging"},
|
||||
s3ActionPutObjectACL: {"s3:PutObjectACL"},
|
||||
s3ActionPutObjectVersionACL: {"s3:PutObjectACL"},
|
||||
s3ActionPutObjectVersionTagging: {"s3:PutObjectTagging"},
|
||||
s3ActionPutObject: {
|
||||
"s3:PutObject", "s3:PostObject", "s3:CopyObject",
|
||||
"s3:UploadPart", "s3:UploadPartCopy", "s3:CreateMultipartUpload", "s3:CompleteMultipartUpload",
|
||||
},
|
||||
specialS3ActionsDeleteObjectVersionTagging: {"s3:DeleteObjectTagging"},
|
||||
specialS3ActionsDeleteObject: {"s3:DeleteObject", "s3:DeleteMultipleObjects"},
|
||||
specialS3ActionsDeleteObjectVersion: {"s3:DeleteObject", "s3:DeleteMultipleObjects"},
|
||||
s3ActionDeleteObjectVersionTagging: {"s3:DeleteObjectTagging"},
|
||||
s3ActionDeleteObject: {"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 {
|
||||
GetUserAddress(account, user string) (string, error)
|
||||
}
|
||||
|
@ -170,10 +160,10 @@ func getS3PrincipalsAndConditionFunc(statement Statement, resolver S3Resolver) (
|
|||
|
||||
return principals, func(principal string) chain.Condition {
|
||||
return chain.Condition{
|
||||
Op: op,
|
||||
Object: chain.ObjectRequest,
|
||||
Key: s3.PropertyKeyOwner,
|
||||
Value: principal,
|
||||
Op: op,
|
||||
Kind: chain.KindRequest,
|
||||
Key: s3.PropertyKeyOwner,
|
||||
Value: principal,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
@ -181,13 +171,19 @@ func getS3PrincipalsAndConditionFunc(statement Statement, resolver S3Resolver) (
|
|||
func convertToS3ChainCondition(c Conditions, resolver S3Resolver) ([]GroupedConditions, error) {
|
||||
return convertToChainConditions(c, func(gr GroupedConditions) (GroupedConditions, error) {
|
||||
for i := range gr.Conditions {
|
||||
if gr.Conditions[i].Key == condKeyAWSPrincipalARN {
|
||||
switch {
|
||||
case gr.Conditions[i].Key == condKeyAWSPrincipalARN:
|
||||
gr.Conditions[i].Key = s3.PropertyKeyOwner
|
||||
val, err := formPrincipalOwner(gr.Conditions[i].Value, resolver)
|
||||
if err != nil {
|
||||
return GroupedConditions{}, err
|
||||
}
|
||||
gr.Conditions[i].Value = val
|
||||
|
||||
case gr.Conditions[i].Key == condKeyAWSMFAPresent:
|
||||
gr.Conditions[i].Key = s3.PropertyKeyAccessBoxAttrMFA
|
||||
case strings.HasPrefix(gr.Conditions[i].Key, condKeyAWSResourceTagPrefix):
|
||||
gr.Conditions[i].Kind = chain.KindResource
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -233,22 +229,41 @@ func validateS3ResourceNames(names []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 {
|
||||
if err := validateAction(action); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if action == Wildcard {
|
||||
return []string{Wildcard}, nil
|
||||
}
|
||||
|
||||
if actions, ok := specialActionToS3OpMap[action]; ok {
|
||||
res = append(res, actions...)
|
||||
} else {
|
||||
res = append(res, action)
|
||||
isIAM, err := validateAction(action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -46,6 +46,8 @@ type (
|
|||
PrincipalType string
|
||||
)
|
||||
|
||||
const policyVersion = "2012-10-17"
|
||||
|
||||
const (
|
||||
GeneralPolicyType PolicyType = iota
|
||||
IdentityBasedPolicyType
|
||||
|
@ -222,11 +224,20 @@ func (p Policy) Validate(typ PolicyType) error {
|
|||
}
|
||||
|
||||
func (p Policy) validate() error {
|
||||
if p.Version != policyVersion {
|
||||
return fmt.Errorf("invalid policy version, expected '%s', actual: '%s'", policyVersion, p.Version)
|
||||
}
|
||||
|
||||
if len(p.Statement) == 0 {
|
||||
return errors.New("'Statement' is missing")
|
||||
}
|
||||
|
||||
sids := make(map[string]struct{}, len(p.Statement))
|
||||
for _, statement := range p.Statement {
|
||||
if _, ok := sids[statement.SID]; ok && statement.SID != "" {
|
||||
return fmt.Errorf("duplicate 'SID': %s", statement.SID)
|
||||
}
|
||||
sids[statement.SID] = struct{}{}
|
||||
if !statement.Effect.IsValid() {
|
||||
return fmt.Errorf("unknown effect: '%s'", statement.Effect)
|
||||
}
|
||||
|
|
|
@ -2,14 +2,13 @@ package iam
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -218,6 +217,7 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "valid permission boundaries",
|
||||
policy: Policy{
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: AllowEffect,
|
||||
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
|
||||
|
@ -230,6 +230,7 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "general invalid effect",
|
||||
policy: Policy{
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: "dummy",
|
||||
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
|
||||
|
@ -242,6 +243,7 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "general invalid principal block",
|
||||
policy: Policy{
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: AllowEffect,
|
||||
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
|
||||
|
@ -256,6 +258,7 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "general invalid not principal",
|
||||
policy: Policy{
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: AllowEffect,
|
||||
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
|
||||
|
@ -269,6 +272,7 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "general invalid principal type",
|
||||
policy: Policy{
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: AllowEffect,
|
||||
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
|
||||
|
@ -282,6 +286,7 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "general invalid action block",
|
||||
policy: Policy{
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: AllowEffect,
|
||||
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
|
||||
|
@ -295,6 +300,7 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "general invalid resource block",
|
||||
policy: Policy{
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: AllowEffect,
|
||||
Resource: []string{Wildcard},
|
||||
|
@ -307,6 +313,7 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "invalid resource block",
|
||||
policy: Policy{
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: AllowEffect,
|
||||
Resource: []string{},
|
||||
|
@ -319,6 +326,7 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "missing resource block",
|
||||
policy: Policy{
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: AllowEffect,
|
||||
}},
|
||||
|
@ -332,9 +340,43 @@ func TestValidatePolicies(t *testing.T) {
|
|||
typ: GeneralPolicyType,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "duplicate sid",
|
||||
policy: Policy{
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{
|
||||
{
|
||||
SID: "sid",
|
||||
Effect: AllowEffect,
|
||||
Action: []string{"s3:*"},
|
||||
Resource: []string{Wildcard},
|
||||
},
|
||||
{
|
||||
SID: "sid",
|
||||
Effect: AllowEffect,
|
||||
Action: []string{"cloudwatch:*"},
|
||||
Resource: []string{Wildcard},
|
||||
}},
|
||||
},
|
||||
typ: GeneralPolicyType,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "missing version",
|
||||
policy: Policy{
|
||||
Statement: []Statement{{
|
||||
Effect: AllowEffect,
|
||||
Action: []string{"s3:*"},
|
||||
Resource: []string{Wildcard},
|
||||
}},
|
||||
},
|
||||
typ: GeneralPolicyType,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
name: "identity based valid",
|
||||
policy: Policy{
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: AllowEffect,
|
||||
Action: []string{"s3:PutObject"},
|
||||
|
@ -347,7 +389,8 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "identity based invalid because of id presence",
|
||||
policy: Policy{
|
||||
ID: "some-id",
|
||||
ID: "some-id",
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: AllowEffect,
|
||||
Action: []string{"s3:PutObject"},
|
||||
|
@ -360,6 +403,7 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "identity based invalid because of principal presence",
|
||||
policy: Policy{
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: AllowEffect,
|
||||
Action: []string{"s3:PutObject"},
|
||||
|
@ -373,6 +417,7 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "identity based invalid because of not principal presence",
|
||||
policy: Policy{
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: AllowEffect,
|
||||
Action: []string{"s3:PutObject"},
|
||||
|
@ -386,6 +431,7 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "resource based valid principal",
|
||||
policy: Policy{
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: DenyEffect,
|
||||
Action: []string{"s3:PutObject"},
|
||||
|
@ -399,7 +445,8 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "resource based valid not principal",
|
||||
policy: Policy{
|
||||
ID: "some-id",
|
||||
ID: "some-id",
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: DenyEffect,
|
||||
Action: []string{"s3:PutObject"},
|
||||
|
@ -413,7 +460,8 @@ func TestValidatePolicies(t *testing.T) {
|
|||
{
|
||||
name: "resource based invalid missing principal",
|
||||
policy: Policy{
|
||||
ID: "some-id",
|
||||
ID: "some-id",
|
||||
Version: policyVersion,
|
||||
Statement: []Statement{{
|
||||
Effect: AllowEffect,
|
||||
Action: []string{"s3:PutObject"},
|
||||
|
@ -476,27 +524,27 @@ func TestProcessDenyFirst(t *testing.T) {
|
|||
|
||||
mockResolver := newMockUserResolver([]string{"root/user-name"}, []string{"test-bucket"}, "")
|
||||
|
||||
identityNativePolicy, err := ConvertToNativeChain(identityPolicy, mockResolver)
|
||||
identityNativePolicy, err := ConvertToS3Chain(identityPolicy, mockResolver)
|
||||
require.NoError(t, err)
|
||||
identityNativePolicy.MatchType = chain.MatchTypeFirstMatch
|
||||
|
||||
resourceNativePolicy, err := ConvertToNativeChain(resourcePolicy, mockResolver)
|
||||
resourceNativePolicy, err := ConvertToS3Chain(resourcePolicy, mockResolver)
|
||||
require.NoError(t, err)
|
||||
|
||||
s := inmemory.NewInMemory()
|
||||
|
||||
target := engine.NamespaceTarget("ns")
|
||||
|
||||
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, identityNativePolicy)
|
||||
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, target, identityNativePolicy)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, resourceNativePolicy)
|
||||
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, target, resourceNativePolicy)
|
||||
require.NoError(t, err)
|
||||
|
||||
resource := testutil.NewResource(fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers["test-bucket"]), nil)
|
||||
request := testutil.NewRequest("PutObject", resource, map[string]string{native.PropertyKeyActorPublicKey: mockResolver.users["root/user-name"]})
|
||||
resource := testutil.NewResource("arn:aws:s3:::test-bucket/object", nil)
|
||||
request := testutil.NewRequest("s3:PutObject", resource, map[string]string{s3.PropertyKeyOwner: mockResolver.users["root/user-name"]})
|
||||
|
||||
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.True(t, found)
|
||||
require.Equal(t, chain.AccessDenied, status)
|
||||
|
|
|
@ -2,10 +2,12 @@ package chain
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
|
@ -66,19 +68,17 @@ type Resources struct {
|
|||
}
|
||||
|
||||
type Condition struct {
|
||||
Op ConditionType
|
||||
Object ObjectType
|
||||
Key string
|
||||
Value string
|
||||
Op ConditionType
|
||||
Kind ConditionKindType
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
type ObjectType byte
|
||||
type ConditionKindType byte
|
||||
|
||||
const (
|
||||
ObjectResource ObjectType = iota
|
||||
ObjectRequest
|
||||
ContainerResource
|
||||
ContainerRequest
|
||||
KindResource ConditionKindType = iota
|
||||
KindRequest
|
||||
)
|
||||
|
||||
type ConditionType byte
|
||||
|
@ -108,6 +108,9 @@ const (
|
|||
CondNumericGreaterThanEquals
|
||||
|
||||
CondSliceContains
|
||||
|
||||
CondIPAddress
|
||||
CondNotIPAddress
|
||||
)
|
||||
|
||||
var condToStr = []struct {
|
||||
|
@ -131,6 +134,8 @@ var condToStr = []struct {
|
|||
{CondNumericGreaterThan, "NumericGreaterThan"},
|
||||
{CondNumericGreaterThanEquals, "NumericGreaterThanEquals"},
|
||||
{CondSliceContains, "SliceContains"},
|
||||
{CondIPAddress, "IPAddress"},
|
||||
{CondNotIPAddress, "NotIPAddress"},
|
||||
}
|
||||
|
||||
func (c ConditionType) String() string {
|
||||
|
@ -152,13 +157,13 @@ func FormCondSliceContainsValue(values []string) string {
|
|||
|
||||
func (c *Condition) Match(req resource.Request) bool {
|
||||
var val string
|
||||
switch c.Object {
|
||||
case ObjectResource:
|
||||
switch c.Kind {
|
||||
case KindResource:
|
||||
val = req.Resource().Property(c.Key)
|
||||
case ObjectRequest:
|
||||
case KindRequest:
|
||||
val = req.Property(c.Key)
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown condition type: %d", c.Object))
|
||||
panic(fmt.Sprintf("unknown condition type: %d", c.Kind))
|
||||
}
|
||||
|
||||
switch c.Op {
|
||||
|
@ -186,6 +191,61 @@ func (c *Condition) Match(req resource.Request) bool {
|
|||
return val >= c.Value
|
||||
case CondSliceContains:
|
||||
return slices.Contains(strings.Split(val, condSliceContainsDelimiter), c.Value)
|
||||
case CondNumericEquals, CondNumericNotEquals, CondNumericLessThan, CondNumericLessThanEquals, CondNumericGreaterThan,
|
||||
CondNumericGreaterThanEquals:
|
||||
return c.matchNumeric(val)
|
||||
case CondIPAddress, CondNotIPAddress:
|
||||
return c.matchIP(val)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Condition) matchNumeric(val string) bool {
|
||||
valDecimal, err := fixedn.Fixed8FromString(val)
|
||||
if err != nil {
|
||||
return c.Op == CondNumericNotEquals
|
||||
}
|
||||
|
||||
condVal, err := fixedn.Fixed8FromString(c.Value)
|
||||
if err != nil {
|
||||
return c.Op == CondNumericNotEquals
|
||||
}
|
||||
|
||||
switch c.Op {
|
||||
default:
|
||||
panic(fmt.Sprintf("unimplemented: %d", c.Op))
|
||||
case CondNumericEquals:
|
||||
return valDecimal.Equal(condVal)
|
||||
case CondNumericNotEquals:
|
||||
return !valDecimal.Equal(condVal)
|
||||
case CondNumericLessThan:
|
||||
return valDecimal.LessThan(condVal)
|
||||
case CondNumericLessThanEquals:
|
||||
return valDecimal.LessThan(condVal) || valDecimal.Equal(condVal)
|
||||
case CondNumericGreaterThan:
|
||||
return valDecimal.GreaterThan(condVal)
|
||||
case CondNumericGreaterThanEquals:
|
||||
return valDecimal.GreaterThan(condVal) || valDecimal.Equal(condVal)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Condition) matchIP(val string) bool {
|
||||
ipAddr, err := netip.ParseAddr(val)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
prefix, err := netip.ParsePrefix(c.Value)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch c.Op {
|
||||
default:
|
||||
panic(fmt.Sprintf("unimplemented: %d", c.Op))
|
||||
case CondIPAddress:
|
||||
return prefix.Contains(ipAddr)
|
||||
case CondNotIPAddress:
|
||||
return !prefix.Contains(ipAddr)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
BIN
pkg/chain/chain_easyjson.go
generated
BIN
pkg/chain/chain_easyjson.go
generated
Binary file not shown.
|
@ -1,11 +1,14 @@
|
|||
package chain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/schema/common"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -49,7 +52,7 @@ func TestEncodeDecode(t *testing.T) {
|
|||
require.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestReturnFirstMatch(t *testing.T) {
|
||||
func TestChainMatch(t *testing.T) {
|
||||
ch := Chain{
|
||||
Rules: []Rule{
|
||||
{
|
||||
|
@ -86,9 +89,195 @@ func TestReturnFirstMatch(t *testing.T) {
|
|||
require.True(t, found)
|
||||
require.Equal(t, Allow, st)
|
||||
})
|
||||
|
||||
t.Run("unknown match", func(t *testing.T) {
|
||||
ch.MatchType = MatchType(255)
|
||||
request := testutil.NewRequest(native.MethodGetObject, resource, nil)
|
||||
require.PanicsWithValue(t, "unknown MatchType 255", func() {
|
||||
ch.Match(request)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("no rule found", func(t *testing.T) {
|
||||
ch.MatchType = MatchTypeFirstMatch
|
||||
request := testutil.NewRequest(native.MethodGetObject, resource, nil)
|
||||
st, found := ch.Match(request)
|
||||
require.False(t, found)
|
||||
require.Equal(t, NoRuleFound, st)
|
||||
})
|
||||
}
|
||||
|
||||
func 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
|
||||
groupID := "1"
|
||||
|
||||
|
@ -97,10 +286,10 @@ func TestCondSliceContainsMatch(t *testing.T) {
|
|||
Actions: Actions{Names: []string{native.MethodPutObject}},
|
||||
Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
|
||||
Condition: []Condition{{
|
||||
Op: CondSliceContains,
|
||||
Object: ObjectRequest,
|
||||
Key: propKey,
|
||||
Value: groupID,
|
||||
Op: CondSliceContains,
|
||||
Kind: KindRequest,
|
||||
Key: propKey,
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -218,7 +218,7 @@ func marshalCondition(buf []byte, offset int, c Condition) (int, error) {
|
|||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
offset, err = marshal.ByteMarshal(buf, offset, byte(c.Object))
|
||||
offset, err = marshal.ByteMarshal(buf, offset, byte(c.Kind))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -241,7 +241,7 @@ func unmarshalCondition(buf []byte, offset int) (Condition, int, error) {
|
|||
if err != nil {
|
||||
return Condition{}, 0, err
|
||||
}
|
||||
c.Object = ObjectType(obV)
|
||||
c.Kind = ConditionKindType(obV)
|
||||
|
||||
c.Key, offset, err = marshal.StringUnmarshal(buf, offset)
|
||||
if err != nil {
|
||||
|
|
|
@ -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")))
|
||||
}
|
||||
|
||||
func FuzzUnmarshal(f *testing.F) {
|
||||
for _, id := range generateTestIDs() {
|
||||
for _, rules := range generateTestRules() {
|
||||
for _, matchType := range generateTestMatchTypes() {
|
||||
|
||||
chain := Chain{
|
||||
ID: id,
|
||||
Rules: rules,
|
||||
MatchType: matchType,
|
||||
}
|
||||
data, err := chain.MarshalBinary()
|
||||
require.NoError(f, err)
|
||||
f.Add(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
var ch Chain
|
||||
require.NotPanics(t, func() {
|
||||
_ = ch.UnmarshalBinary(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func performMarshalTest(t *testing.T, id ID, r []Rule, mt MatchType) {
|
||||
chain := Chain{
|
||||
ID: id,
|
||||
|
@ -203,31 +178,31 @@ func generateTestConditions() [][]Condition {
|
|||
for _, ct := range generateTestConditionTypes() {
|
||||
for _, ot := range generateObjectTypes() {
|
||||
result[2] = append(result[2], Condition{
|
||||
Op: ct,
|
||||
Object: ot,
|
||||
Key: "",
|
||||
Value: "",
|
||||
Op: ct,
|
||||
Kind: ot,
|
||||
Key: "",
|
||||
Value: "",
|
||||
})
|
||||
|
||||
result[2] = append(result[2], Condition{
|
||||
Op: ct,
|
||||
Object: ot,
|
||||
Key: "key",
|
||||
Value: "",
|
||||
Op: ct,
|
||||
Kind: ot,
|
||||
Key: "key",
|
||||
Value: "",
|
||||
})
|
||||
|
||||
result[2] = append(result[2], Condition{
|
||||
Op: ct,
|
||||
Object: ot,
|
||||
Key: "",
|
||||
Value: "value",
|
||||
Op: ct,
|
||||
Kind: ot,
|
||||
Key: "",
|
||||
Value: "value",
|
||||
})
|
||||
|
||||
result[2] = append(result[2], Condition{
|
||||
Op: ct,
|
||||
Object: ot,
|
||||
Key: "key",
|
||||
Value: "value",
|
||||
Op: ct,
|
||||
Kind: ot,
|
||||
Key: "key",
|
||||
Value: "value",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -257,10 +232,10 @@ func generateTestConditionTypes() []ConditionType {
|
|||
}
|
||||
}
|
||||
|
||||
func generateObjectTypes() []ObjectType {
|
||||
return []ObjectType{
|
||||
ObjectResource,
|
||||
ObjectRequest,
|
||||
func generateObjectTypes() []ConditionKindType {
|
||||
return []ConditionKindType{
|
||||
KindResource,
|
||||
KindRequest,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
13
pkg/chain/marshal_fuzz.go
Normal file
13
pkg/chain/marshal_fuzz.go
Normal 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
|
||||
}
|
34
pkg/chain/marshal_fuzz_test.go
Normal file
34
pkg/chain/marshal_fuzz_test.go
Normal 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)
|
||||
})
|
||||
})
|
||||
}
|
|
@ -29,11 +29,11 @@ var statusToJSONValue = []struct {
|
|||
}
|
||||
|
||||
var objectTypeToJSONValue = []struct {
|
||||
t ObjectType
|
||||
t ConditionKindType
|
||||
str string
|
||||
}{
|
||||
{ObjectRequest, "Request"},
|
||||
{ObjectResource, "Resource"},
|
||||
{KindRequest, "Request"},
|
||||
{KindResource, "Resource"},
|
||||
}
|
||||
|
||||
func (mt MatchType) MarshalEasyJSON(w *jwriter.Writer) {
|
||||
|
@ -90,7 +90,7 @@ func (st *Status) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
|||
*st = Status(v)
|
||||
}
|
||||
|
||||
func (ot ObjectType) MarshalEasyJSON(w *jwriter.Writer) {
|
||||
func (ot ConditionKindType) MarshalEasyJSON(w *jwriter.Writer) {
|
||||
for _, p := range objectTypeToJSONValue {
|
||||
if p.t == ot {
|
||||
w.String(p.str)
|
||||
|
@ -100,7 +100,7 @@ func (ot ObjectType) MarshalEasyJSON(w *jwriter.Writer) {
|
|||
w.String(strconv.FormatUint(uint64(ot), 10))
|
||||
}
|
||||
|
||||
func (ot *ObjectType) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||
func (ot *ConditionKindType) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||
str := l.String()
|
||||
for _, p := range objectTypeToJSONValue {
|
||||
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))
|
||||
return
|
||||
}
|
||||
*ot = ObjectType(v)
|
||||
*ot = ConditionKindType(v)
|
||||
}
|
||||
|
||||
func (ct ConditionType) MarshalEasyJSON(w *jwriter.Writer) {
|
||||
|
|
|
@ -68,10 +68,10 @@ func TestJsonEnums(t *testing.T) {
|
|||
},
|
||||
Condition: []Condition{
|
||||
{
|
||||
Op: CondStringEquals,
|
||||
Object: ObjectRequest,
|
||||
Key: native.PropertyKeyActorRole,
|
||||
Value: native.PropertyValueContainerRoleOthers,
|
||||
Op: CondStringEquals,
|
||||
Kind: KindRequest,
|
||||
Key: native.PropertyKeyActorRole,
|
||||
Value: native.PropertyValueContainerRoleOthers,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -87,10 +87,10 @@ func TestJsonEnums(t *testing.T) {
|
|||
Any: true,
|
||||
Condition: []Condition{
|
||||
{
|
||||
Op: CondStringNotLike,
|
||||
Object: ObjectResource,
|
||||
Key: native.PropertyKeyObjectType,
|
||||
Value: "regular",
|
||||
Op: CondStringNotLike,
|
||||
Kind: KindResource,
|
||||
Key: native.PropertyKeyObjectType,
|
||||
Value: "regular",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -98,8 +98,8 @@ func TestJsonEnums(t *testing.T) {
|
|||
Status: Status(100),
|
||||
Condition: []Condition{
|
||||
{
|
||||
Op: ConditionType(255),
|
||||
Object: ObjectType(128),
|
||||
Op: ConditionType(255),
|
||||
Kind: ConditionKindType(128),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
6
pkg/chain/testdata/test_status_json.json
vendored
6
pkg/chain/testdata/test_status_json.json
vendored
|
@ -20,7 +20,7 @@
|
|||
"Condition": [
|
||||
{
|
||||
"Op": "StringEquals",
|
||||
"Object": "Request",
|
||||
"Kind": "Request",
|
||||
"Key": "$Actor:role",
|
||||
"Value": "others"
|
||||
}
|
||||
|
@ -44,7 +44,7 @@
|
|||
"Condition": [
|
||||
{
|
||||
"Op": "StringNotLike",
|
||||
"Object": "Resource",
|
||||
"Kind": "Resource",
|
||||
"Key": "$Object:objectType",
|
||||
"Value": "regular"
|
||||
}
|
||||
|
@ -64,7 +64,7 @@
|
|||
"Condition": [
|
||||
{
|
||||
"Op": "255",
|
||||
"Object": "128",
|
||||
"Kind": "128",
|
||||
"Key": "",
|
||||
"Value": ""
|
||||
}
|
||||
|
|
|
@ -42,43 +42,36 @@ func (dr *defaultChainRouter) checkLocal(name chain.Name, rt RequestTarget, r re
|
|||
if dr.local == nil {
|
||||
return
|
||||
}
|
||||
var ruleFounds []bool
|
||||
|
||||
var hasAllow bool
|
||||
for _, target := range rt.Targets() {
|
||||
status, ruleFound, err = dr.matchLocalOverrides(name, target, r)
|
||||
if err != nil || ruleFound && status != chain.Allow {
|
||||
return
|
||||
}
|
||||
ruleFounds = append(ruleFounds, ruleFound)
|
||||
hasAllow = hasAllow || ruleFound
|
||||
}
|
||||
|
||||
status = chain.NoRuleFound
|
||||
for _, ruleFound = range ruleFounds {
|
||||
if ruleFound {
|
||||
status = chain.Allow
|
||||
break
|
||||
}
|
||||
if hasAllow {
|
||||
return chain.Allow, true, nil
|
||||
}
|
||||
return
|
||||
return chain.NoRuleFound, false, nil
|
||||
}
|
||||
|
||||
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() {
|
||||
status, ruleFound, err = dr.matchMorphRuleChains(name, target, r)
|
||||
if err != nil || ruleFound && status != chain.Allow {
|
||||
return
|
||||
}
|
||||
ruleFounds = append(ruleFounds, ruleFound)
|
||||
hasAllow = hasAllow || ruleFound
|
||||
}
|
||||
|
||||
status = chain.NoRuleFound
|
||||
for _, ruleFound = range ruleFounds {
|
||||
if ruleFound {
|
||||
status = chain.Allow
|
||||
break
|
||||
}
|
||||
if hasAllow {
|
||||
return chain.Allow, true, nil
|
||||
}
|
||||
return
|
||||
return chain.NoRuleFound, false, nil
|
||||
}
|
||||
|
||||
func (dr *defaultChainRouter) matchLocalOverrides(name chain.Name, target Target, r resource.Request) (status chain.Status, ruleFound bool, err error) {
|
||||
|
|
|
@ -11,6 +11,69 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAddRootOverrides(t *testing.T) {
|
||||
s := NewInMemoryLocalOverrides()
|
||||
|
||||
target := engine.NamespaceTarget("")
|
||||
|
||||
id, err := s.LocalStorage().AddOverride(chain.S3, target, &chain.Chain{
|
||||
Rules: []chain.Rule{{
|
||||
Status: chain.Allow,
|
||||
Actions: chain.Actions{Names: []string{"s3:PutObject"}},
|
||||
Resources: chain.Resources{Names: []string{"*"}},
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := s.LocalStorage().ListOverrides(chain.S3, target)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, string(id), string(res[0].ID))
|
||||
}
|
||||
|
||||
func TestInmemory_MultipleTargets(t *testing.T) {
|
||||
const op = "ape::test::op"
|
||||
|
||||
targets := []engine.Target{
|
||||
engine.NamespaceTarget("ns1"),
|
||||
engine.ContainerTarget("cnr1"),
|
||||
engine.GroupTarget("group1"),
|
||||
engine.UserTarget("user1"),
|
||||
}
|
||||
req := resourcetest.NewRequest(op, resourcetest.NewResource("r", nil), nil)
|
||||
target := engine.NewRequestTargetExtended("ns1", "cnr1", "user1", []string{"group1"})
|
||||
for _, tt := range targets {
|
||||
t.Run("morph", func(t *testing.T) {
|
||||
s := NewInMemoryLocalOverrides()
|
||||
s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, tt, &chain.Chain{
|
||||
Rules: []chain.Rule{{
|
||||
Status: chain.Allow,
|
||||
Actions: chain.Actions{Names: []string{op}},
|
||||
}},
|
||||
})
|
||||
|
||||
status, found, err := s.IsAllowed(chain.Ingress, target, req)
|
||||
require.NoError(t, err)
|
||||
require.True(t, found)
|
||||
require.Equal(t, chain.Allow, status)
|
||||
})
|
||||
t.Run("override", func(t *testing.T) {
|
||||
s := NewInMemoryLocalOverrides()
|
||||
s.LocalStorage().AddOverride(chain.Ingress, tt, &chain.Chain{
|
||||
Rules: []chain.Rule{{
|
||||
Status: chain.Allow,
|
||||
Actions: chain.Actions{Names: []string{op}},
|
||||
}},
|
||||
})
|
||||
|
||||
status, found, err := s.IsAllowed(chain.Ingress, target, req)
|
||||
require.NoError(t, err)
|
||||
require.True(t, found)
|
||||
require.Equal(t, chain.Allow, status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInmemory(t *testing.T) {
|
||||
const (
|
||||
object = "native::object::abc/xyz"
|
||||
|
@ -49,16 +112,16 @@ func TestInmemory(t *testing.T) {
|
|||
Any: true,
|
||||
Condition: []chain.Condition{
|
||||
{
|
||||
Op: chain.CondStringNotLike,
|
||||
Object: chain.ObjectRequest,
|
||||
Key: "SourceIP",
|
||||
Value: "10.1.1.*",
|
||||
Op: chain.CondStringNotLike,
|
||||
Kind: chain.KindRequest,
|
||||
Key: "SourceIP",
|
||||
Value: "10.1.1.*",
|
||||
},
|
||||
{
|
||||
Op: chain.CondStringNotEquals,
|
||||
Object: chain.ObjectRequest,
|
||||
Key: "Actor",
|
||||
Value: actor1,
|
||||
Op: chain.CondStringNotEquals,
|
||||
Kind: chain.KindRequest,
|
||||
Key: "Actor",
|
||||
Value: actor1,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -91,16 +154,16 @@ func TestInmemory(t *testing.T) {
|
|||
Resources: chain.Resources{Names: []string{"native::object::abc/*"}},
|
||||
Condition: []chain.Condition{
|
||||
{
|
||||
Op: chain.CondStringEquals,
|
||||
Object: chain.ObjectResource,
|
||||
Key: "Department",
|
||||
Value: "HR",
|
||||
Op: chain.CondStringEquals,
|
||||
Kind: chain.KindResource,
|
||||
Key: "Department",
|
||||
Value: "HR",
|
||||
},
|
||||
{
|
||||
Op: chain.CondStringEquals,
|
||||
Object: chain.ObjectRequest,
|
||||
Key: "Actor",
|
||||
Value: actor2,
|
||||
Op: chain.CondStringEquals,
|
||||
Kind: chain.KindRequest,
|
||||
Key: "Actor",
|
||||
Value: actor2,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -52,6 +52,10 @@ func (s *inmemoryLocalStorage) AddOverride(name chain.Name, target engine.Target
|
|||
s.guard.Lock()
|
||||
defer s.guard.Unlock()
|
||||
|
||||
if target.Name == "" {
|
||||
target.Name = "root"
|
||||
}
|
||||
|
||||
// AddOverride assigns generated chain ID if it has not been assigned.
|
||||
if len(c.ID) == 0 {
|
||||
c.ID = s.generateChainID(name, target)
|
||||
|
|
|
@ -21,7 +21,7 @@ func NewInmemoryMorphRuleChainStorage() engine.MorphRuleChainStorage {
|
|||
|
||||
func (s *inmemoryMorphRuleChainStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (_ util.Uint256, _ uint32, err error) {
|
||||
switch target.Type {
|
||||
case engine.Namespace, engine.Container:
|
||||
case engine.Namespace, engine.Container, engine.User, engine.Group:
|
||||
_, err = s.storage.AddOverride(name, target, c)
|
||||
default:
|
||||
err = engine.ErrUnknownTarget
|
||||
|
@ -31,7 +31,7 @@ func (s *inmemoryMorphRuleChainStorage) AddMorphRuleChain(name chain.Name, targe
|
|||
|
||||
func (s *inmemoryMorphRuleChainStorage) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (_ util.Uint256, _ uint32, err error) {
|
||||
switch target.Type {
|
||||
case engine.Namespace, engine.Container:
|
||||
case engine.Namespace, engine.Container, engine.User, engine.Group:
|
||||
err = s.storage.RemoveOverride(name, target, chainID)
|
||||
default:
|
||||
err = engine.ErrUnknownTarget
|
||||
|
@ -41,7 +41,7 @@ func (s *inmemoryMorphRuleChainStorage) RemoveMorphRuleChain(name chain.Name, ta
|
|||
|
||||
func (s *inmemoryMorphRuleChainStorage) RemoveMorphRuleChainsByTarget(name chain.Name, target engine.Target) (_ util.Uint256, _ uint32, err error) {
|
||||
switch target.Type {
|
||||
case engine.Namespace, engine.Container:
|
||||
case engine.Namespace, engine.Container, engine.User, engine.Group:
|
||||
err = s.storage.RemoveOverridesByTarget(name, target)
|
||||
default:
|
||||
err = engine.ErrUnknownTarget
|
||||
|
@ -51,7 +51,7 @@ func (s *inmemoryMorphRuleChainStorage) RemoveMorphRuleChainsByTarget(name chain
|
|||
|
||||
func (s *inmemoryMorphRuleChainStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
|
||||
switch target.Type {
|
||||
case engine.Namespace, engine.Container:
|
||||
case engine.Namespace, engine.Container, engine.User, engine.Group:
|
||||
return s.storage.ListOverrides(name, target)
|
||||
default:
|
||||
}
|
||||
|
|
|
@ -37,6 +37,8 @@ type TargetType rune
|
|||
const (
|
||||
Namespace TargetType = 'n'
|
||||
Container TargetType = 'c'
|
||||
User TargetType = 'u'
|
||||
Group TargetType = 'g'
|
||||
)
|
||||
|
||||
type Target struct {
|
||||
|
@ -48,6 +50,8 @@ type Target struct {
|
|||
type RequestTarget struct {
|
||||
Namespace *Target
|
||||
Container *Target
|
||||
User *Target
|
||||
Groups []Target
|
||||
}
|
||||
|
||||
func NewRequestTargetWithNamespace(namespace string) RequestTarget {
|
||||
|
@ -73,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) {
|
||||
if rt.Namespace != nil {
|
||||
targets = append(targets, *rt.Namespace)
|
||||
|
@ -80,6 +102,12 @@ func (rt *RequestTarget) Targets() (targets []Target) {
|
|||
if rt.Container != nil {
|
||||
targets = append(targets, *rt.Container)
|
||||
}
|
||||
if rt.User != nil {
|
||||
targets = append(targets, *rt.User)
|
||||
}
|
||||
if len(rt.Groups) != 0 {
|
||||
targets = append(targets, rt.Groups...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -97,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
|
||||
// data like chains, target or admin from a chain storage.
|
||||
type MorphRuleChainStorageReader interface {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"math/big"
|
||||
"strings"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/commonclient"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/policy"
|
||||
client "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/policy"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
|
@ -13,6 +14,7 @@ import (
|
|||
"github.com/google/uuid"
|
||||
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
||||
neoinvoker "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
|
@ -26,6 +28,10 @@ var (
|
|||
|
||||
// ContractStorage is the interface to manage chain rules within Policy contract.
|
||||
type ContractStorage struct {
|
||||
hash util.Uint160
|
||||
|
||||
actor ContractStorageActor
|
||||
|
||||
contractInterface *client.Contract
|
||||
}
|
||||
|
||||
|
@ -33,23 +39,49 @@ var _ engine.MorphRuleChainStorage = (*ContractStorage)(nil)
|
|||
|
||||
// ContractStorageReader is the interface to read data from Policy contract.
|
||||
type ContractStorageReader struct {
|
||||
hash util.Uint160
|
||||
|
||||
invoker ContractStorageInvoker
|
||||
|
||||
contractReaderInterface *client.ContractReader
|
||||
}
|
||||
|
||||
type ContractStorageActor interface {
|
||||
client.Actor
|
||||
GetRPCInvoker() neoinvoker.RPCInvoke
|
||||
}
|
||||
|
||||
var _ engine.MorphRuleChainStorageReader = (*ContractStorageReader)(nil)
|
||||
|
||||
func NewContractStorage(actor client.Actor, contract util.Uint160) *ContractStorage {
|
||||
func NewContractStorage(actor ContractStorageActor, contract util.Uint160) *ContractStorage {
|
||||
return &ContractStorage{
|
||||
hash: contract,
|
||||
actor: actor,
|
||||
contractInterface: client.New(actor, contract),
|
||||
}
|
||||
}
|
||||
|
||||
type contractStorageActorImpl struct {
|
||||
client.Actor
|
||||
rpcActor actor.RPCActor
|
||||
}
|
||||
|
||||
var _ ContractStorageActor = &contractStorageActorImpl{}
|
||||
|
||||
func (c *contractStorageActorImpl) GetRPCInvoker() neoinvoker.RPCInvoke {
|
||||
return c.rpcActor
|
||||
}
|
||||
|
||||
// NewContractStorageWithSimpleActor constructs core actor from `rpcActor`.
|
||||
//
|
||||
// Note: NewContractStorageWithSimpleActor is appropriate only for call-only-once cases (for example, in CLIs). Otherwise, it is unsafe,
|
||||
// because core actor may use invalidated `rpcActor` if some connection errors occurred.
|
||||
func NewContractStorageWithSimpleActor(rpcActor actor.RPCActor, acc *wallet.Account, contract util.Uint160) (*ContractStorage, error) {
|
||||
act, err := actor.NewSimple(rpcActor, acc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create simple actor: %w", err)
|
||||
}
|
||||
return NewContractStorage(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) {
|
||||
|
@ -98,17 +130,26 @@ func (s *ContractStorage) RemoveMorphRuleChainsByTarget(name chain.Name, target
|
|||
return
|
||||
}
|
||||
|
||||
func (s *ContractStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
|
||||
func listChains(name chain.Name, target engine.Target, rpcInvoker neoinvoker.RPCInvoke, hash util.Uint160) ([]*chain.Chain, error) {
|
||||
kind, err := policyKind(target.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items, err := s.contractInterface.ListChainsByPrefix(big.NewInt(int64(kind)), target.Name, []byte(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
const (
|
||||
method = "iteratorChainsByPrefix"
|
||||
batchSize = neoinvoker.DefaultIteratorResultItems
|
||||
)
|
||||
|
||||
inv := neoinvoker.New(rpcInvoker, nil)
|
||||
params := []any{
|
||||
big.NewInt(int64(kind)), target.Name, []byte(name),
|
||||
}
|
||||
|
||||
items, err := commonclient.ReadIteratorItems(inv, batchSize, hash, method, params...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read items error: %w", err)
|
||||
}
|
||||
var chains []*chain.Chain
|
||||
for _, item := range items {
|
||||
serialized, err := bytesFromStackItem(item)
|
||||
|
@ -121,10 +162,13 @@ func (s *ContractStorage) ListMorphRuleChains(name chain.Name, target engine.Tar
|
|||
}
|
||||
chains = append(chains, c)
|
||||
}
|
||||
|
||||
return chains, nil
|
||||
}
|
||||
|
||||
func (s *ContractStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
|
||||
return listChains(name, target, s.actor.GetRPCInvoker(), s.hash)
|
||||
}
|
||||
|
||||
func (s *ContractStorage) ListTargetsIterator(targetType engine.TargetType) (uuid.UUID, result.Iterator, error) {
|
||||
kind, err := policyKind(targetType)
|
||||
if err != nil {
|
||||
|
@ -141,37 +185,21 @@ func (s *ContractStorage) SetAdmin(addr util.Uint160) (util.Uint256, uint32, err
|
|||
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{
|
||||
hash: contract,
|
||||
invoker: inv,
|
||||
contractReaderInterface: client.NewReader(inv, contract),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ContractStorageReader) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
|
||||
kind, err := policyKind(target.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items, err := s.contractReaderInterface.ListChainsByPrefix(big.NewInt(int64(kind)), target.Name, []byte(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var chains []*chain.Chain
|
||||
for _, item := range items {
|
||||
serialized, err := bytesFromStackItem(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := new(chain.Chain)
|
||||
if err := c.DecodeBytes(serialized); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chains = append(chains, c)
|
||||
}
|
||||
|
||||
return chains, nil
|
||||
return listChains(name, target, s.invoker.GetRPCInvoker(), s.hash)
|
||||
}
|
||||
|
||||
func (s *ContractStorageReader) GetAdmin() (util.Uint160, error) {
|
||||
|
@ -205,10 +233,16 @@ func prefixedChainName(name chain.Name, chainID chain.ID) []byte {
|
|||
}
|
||||
|
||||
func policyKind(typ engine.TargetType) (policy.Kind, error) {
|
||||
if typ == engine.Namespace {
|
||||
switch typ {
|
||||
case engine.Namespace:
|
||||
return policy.Namespace, nil
|
||||
} else if typ == engine.Container {
|
||||
case engine.Container:
|
||||
return policy.Container, nil
|
||||
case engine.User:
|
||||
return policy.Kind(engine.User), nil
|
||||
case engine.Group:
|
||||
return policy.Kind(engine.Group), nil
|
||||
default:
|
||||
return policy.Kind(0), ErrEngineTargetTypeUnsupported
|
||||
}
|
||||
return policy.Kind(0), ErrEngineTargetTypeUnsupported
|
||||
}
|
||||
|
|
|
@ -2,4 +2,10 @@ package common
|
|||
|
||||
const (
|
||||
PropertyKeyFrostFSIDGroupID = "frostfsid:groupID"
|
||||
|
||||
PropertyKeyFrostFSSourceIP = "frostfs:sourceIP"
|
||||
|
||||
PropertyKeyFormatFrostFSIDUserClaim = "frostfsid:userClaim/%s"
|
||||
|
||||
PropertyKeyFrostFSXHeader = "frostfs:xheader/%s"
|
||||
)
|
||||
|
|
|
@ -8,6 +8,7 @@ const (
|
|||
MethodSearchObject = "SearchObject"
|
||||
MethodRangeObject = "RangeObject"
|
||||
MethodHashObject = "HashObject"
|
||||
MethodPatchObject = "PatchObject"
|
||||
|
||||
MethodPutContainer = "PutContainer"
|
||||
MethodDeleteContainer = "DeleteContainer"
|
||||
|
|
|
@ -6,6 +6,13 @@ const (
|
|||
PropertyKeyDelimiter = "s3:delimiter"
|
||||
PropertyKeyPrefix = "s3:prefix"
|
||||
PropertyKeyVersionID = "s3:VersionId"
|
||||
PropertyKeyMaxKeys = "s3:max-keys"
|
||||
|
||||
PropertyKeyFormatResourceTag = "aws:ResourceTag/%s"
|
||||
PropertyKeyFormatRequestTag = "aws:RequestTag/%s"
|
||||
|
||||
PropertyKeyAccessBoxAttrMFA = "AccessBox-Attribute/IAM-MFA"
|
||||
PropertyKeyFormatAccessBoxAttr = "AccessBox-Attribute/%s"
|
||||
|
||||
ResourceFormatS3All = "arn:aws:s3:::*"
|
||||
ResourceFormatS3Bucket = "arn:aws:s3:::%s"
|
||||
|
|
Loading…
Reference in a new issue