Compare commits

...

90 commits
init ... master

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

Found by @mbiryukova.
Introduced in #86.

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2024-03-05 10:17:28 +03:00
aarifullin
839f22e1a3 [#55] router: Inmemory implementation should take empty name for "root"
Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2024-03-04 18:12:11 +03:00
aarifullin
cf1f091e26 [#54] morph: Introduce ContractStorageReader
* Implement MorphRuleChainStorageReader interface to make
  possible to read from Policy contract without wallets.

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2024-03-01 08:05:25 +00:00
aarifullin
9e66ce59c6 [#54] morph: Revise MorphRuleChainStorage interface
* Split MorphRuleChainStorage interface by moving read-only
  methods to a separate interface MorphRuleChainStorageReader.

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2024-03-01 08:05:25 +00:00
c960b1b088 [#53] iam: Extend support s3 to native actions
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-02-26 12:42:15 +03:00
aarifullin
8354a074c4 [#49] engine: Fix target considering order
* Namespace target rules should be considered first

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2024-02-06 14:12:36 +03:00
4a989d6bb7 [#50] .fordejo: Update DCO action
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-02-06 12:58:42 +03:00
0edc002441 [#46] iam: Handle s3 complex actions
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-02-01 17:19:00 +03:00
1cdb3e5a4a [#46] iam: Support more s3 to native actions mapping
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-02-01 17:18:55 +03:00
af388779a3 [#46] iam: Shrink rules for wildcard cases
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-01-29 11:50:24 +03:00
8cc5173d73 [#46] iam: Support namespaces when forming native rules
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-01-29 11:50:24 +03:00
2af381ae81 [#46] iam: Error if policy doesn't have actions
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-01-29 11:50:24 +03:00
8d21ab2d99 [#43] engine: Extend with target listing method
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2024-01-29 09:41:40 +03:00
0a28f0a992 [#1] gitattributes: Add easyjson files rules
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2024-01-24 11:04:03 +03:00
dd0f582fc3 [#1] chain: Fix ID type from string to bytes
ID may be non UTF-8 string, so from developers POV
it is just byte slice.

Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2024-01-24 11:04:03 +03:00
5f13d91c0d [#1] native: Fix typo in owner value
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2024-01-24 11:04:03 +03:00
88c2a476b0 [#1] chain: Add json marshal/unmarshal
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2024-01-24 11:04:03 +03:00
58386edf58 [#1] chain: Add binary marshal/unmarshal
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2024-01-24 11:04:03 +03:00
06cbfe8691 [#876] policy: Add resource\request for container
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2024-01-22 13:47:24 +03:00
c80c99b13e [#41] chain: Fix ID serialization
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-01-12 10:56:04 +03:00
ed93bb5cc5 [#35] local_storage: Make thread safe
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-12-21 12:13:54 +00:00
06e9c91014 [#33] pkg/chain: Support CondSliceContains condition
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-12-21 11:13:52 +00:00
b82544b0fe [#876] policy: Fix SetAdmin
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2023-12-21 07:54:42 +03:00
641a1429ef [#876] policy: Add methods Get/SetAdmin for wrapper
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2023-12-21 04:51:21 +00:00
02e50307df [#34] native: Add container methods
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-12-20 14:22:01 +03:00
3128352693 [#36] iam: Keep s3/iam prefixes in resources
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-12-20 07:08:31 +00:00
ec39d8371a [#36] iam: Support iam actions
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-12-20 07:08:31 +00:00
aarifullin
e57d213595 [#26] schema: Add resource name validation method
Close #26

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2023-12-19 16:40:59 +00:00
aarifullin
62ea96b82c [#32] morph: Remove name transformer in morph policy client
* It is not required to transform long names because
  container chains will be added by container ID
  but not by a resource name.

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2023-12-14 12:22:53 +00:00
1d07331f5d [#28] iam: Fix converters
Handle resource without object as bucket name instead of bucket with any object

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-12-13 13:20:38 +00:00
3b107e9413 [#28] chain: Add S3 chain name
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-12-13 13:20:38 +00:00
8c673ee4f4 [#21] chain: Allow to return first match result
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-12-11 11:03:03 +03:00
1375e8f7fd [#21] router: Make Deny the highest priority
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-12-08 12:37:29 +03:00
aarifullin
156018bcba [#3] policy: Introduce policy contract interface wrapper
Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2023-12-07 14:01:27 +03:00
aarifullin
df15b38c63 [#3] engine: Refactor MorphRuleChainStorage
* Make changing state methods like AddMorphRuleChain,
  RemoveMorphRuleChain return transaction hash and VUB.

Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2023-12-06 12:25:55 +03:00
aarifullin
2d4a9fc6dc [#25] engine: Refactor ChainRouter interface
* Pass RequestTarget instead only namespace
* Refactor unit-tests and dependencies

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2023-12-05 09:20:54 +00:00
aarifullin
4d8242584a [#25] engine: Refactor LocalOverrideStorage
* Make LocalOverrideStorage methods to receive Target type
  instead resource
* Refactor unit-tests and dependencies
* Make default chain router check local overrides not
  only for container but also for namespaces

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2023-12-05 09:20:54 +00:00
a0a35bf4bf [#22] iam: Fix converters
Validate that actions and resources contain wildcard only at the end

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-11-28 17:56:36 +03:00
5fa9d91903 [#17] iam: Add converter to native/s3 policy
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-11-21 11:45:41 +03:00
5db67021e1 [#20] schema: Drop root from namespace definition
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-11-15 12:47:36 +03:00
aarifullin
17453d3cda [#7] engine: Revise CachedChainStorage interface
* Nuke out CachedChainStorage interface
* Introduce LocalOverrideStorage interface to manage
  local overrides
* Introduce MorphRuleChainStorage interface to manage
  chains in the policy contract
* Extend Engine interface

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2023-11-15 09:22:42 +00:00
aarifullin
a08f600d97 [#7] engine: Set project structure pattern for files
* Create pkg package
* Move chain-relates structures to pkg/chain package
* Move inmemory and interface files to pkg/engine package
* Move resource structures to pkg/resource package
* Move GlobMatch to util package

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2023-11-15 09:22:42 +00:00
aarifullin
9472a7123e [#7] engine: Move globMatch to common util package
Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2023-11-15 09:22:42 +00:00
38985e4ec8 [#19] schema: Add native schema consts
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-11-14 13:09:51 +03:00
b7645489d0 [#18] chain: Drop Actor object type
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-11-13 17:41:08 +03:00
63ecf63a08 [#11] iam: Support 'NotPrincipal', 'NotAction', 'NotResource'
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-11-02 14:55:48 +03:00
8d291039d8 [#11] Support inverted action and resource
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-11-02 12:24:37 +03:00
5eee1a7334 [#14] pre-commit: Add gofumpt
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-11-01 11:24:25 +03:00
8dc9d9fa58 [#14] .forgejo: Add tests and lint workflows
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-11-01 11:18:57 +03:00
7f6ee39cb8 [#14] Fix linter warnings
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-11-01 11:05:03 +03:00
aarifullin
76372aac04 [#13] interface: Add methods to CachedChainStorage interface
* Introduce GetOverride, RemoveOverride, ListOverrides
* Implement them in inmemory struct

Signed-off-by: aarifullin <aarifullin@yadro.com>
2023-10-31 17:21:34 +03:00
aarifullin
35f24627f0 [#13] chain: Introduce ChainID type
Signed-off-by: aarifullin <aarifullin@yadro.com>
2023-10-31 17:21:28 +03:00
31a308ea61 [#4] Reduce number of condition types 2023-10-23 15:44:34 +03:00
88cf807951 [#4] Add IAM policy unmarshaler 2023-10-23 15:44:34 +03:00
5ebb2e694c [#2] Initial implementation
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-10-23 10:45:15 +03:00
52 changed files with 9834 additions and 14 deletions

View file

@ -0,0 +1,21 @@
name: DCO action
on: [pull_request]
jobs:
dco:
name: DCO
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: '1.23'
- name: Run commit format checker
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v3
with:
from: 'origin/${{ github.event.pull_request.base.ref }}'

View file

@ -0,0 +1,73 @@
name: Tests and linters
on: [pull_request]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '1.23'
cache: true
- name: Install linters
run: make lint-install
- name: Run linters
run: make lint
tests:
name: Tests
runs-on: ubuntu-latest
strategy:
matrix:
go_versions: [ '1.22', '1.23' ]
fail-fast: false
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '${{ matrix.go_versions }}'
cache: true
- name: Run tests
run: make test
tests-race:
name: Tests with -race
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '1.23'
cache: true
- name: Run tests
run: go test ./... -count=1 -race
staticcheck:
name: Staticcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '1.23'
cache: true
- name: Install staticcheck
run: make staticcheck-install
- name: Run staticcheck
run: make staticcheck-run

2
.gitattributes vendored
View file

@ -1,2 +1,4 @@
/**/*.pb.go -diff -merge /**/*.pb.go -diff -merge
/**/*.pb.go linguist-generated=true /**/*.pb.go linguist-generated=true
/**/*_easyjson.go -diff -merge
/**/*_easyjson.go linguist-generated=true

View file

@ -12,7 +12,8 @@ run:
# output configuration options # output configuration options
output: output:
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
format: tab formats:
- format: tab
# all available settings of specific linters # all available settings of specific linters
linters-settings: linters-settings:
@ -47,7 +48,7 @@ linters:
- durationcheck - durationcheck
- exhaustive - exhaustive
- exportloopref - exportloopref
- gofmt - gofumpt
- goimports - goimports
- misspell - misspell
- whitespace - whitespace

View file

@ -2,13 +2,6 @@ ci:
autofix_prs: false autofix_prs: false
repos: repos:
- repo: https://github.com/jorisroovers/gitlint
rev: v0.19.1
hooks:
- id: gitlint
stages: [commit-msg]
- id: gitlint-ci
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.4.0
hooks: hooks:
@ -26,20 +19,29 @@ repos:
exclude: ".key$" exclude: ".key$"
- repo: https://github.com/shellcheck-py/shellcheck-py - repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.2 rev: v0.9.0.6
hooks: hooks:
- id: shellcheck - id: shellcheck
- repo: https://github.com/golangci/golangci-lint - repo: local
rev: v1.51.2
hooks: hooks:
- id: golangci-lint - id: make-lint
name: Run Make Lint
entry: make lint
language: system
pass_filenames: false
- repo: local - repo: local
hooks: hooks:
- id: go-unit-tests - id: go-unit-tests
name: go unit tests name: go unit tests
entry: make test entry: make test GOFLAGS=''
pass_filenames: false pass_filenames: false
types: [go] types: [go]
language: system language: system
- repo: https://github.com/TekWizely/pre-commit-golang
rev: v1.0.0-rc.1
hooks:
- id: go-staticcheck-repo-mod
- id: go-mod-tidy

77
Makefile Executable file
View file

@ -0,0 +1,77 @@
#!/usr/bin/make -f
TRUECLOUDLAB_LINT_VERSION ?= 0.0.6
TMP_DIR := .cache
OUTPUT_LINT_DIR ?= $(shell pwd)/bin
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)
# Run all code formatters
fmts: fmt imports
# Reformat code
fmt:
@echo "⇒ Processing gofmt check"
@gofumpt -s -w .
# Reformat imports
imports:
@echo "⇒ Processing goimports check"
@goimports -w .
# Run Unit Test with go test
test: GOFLAGS ?= "-count=1"
test:
@echo "⇒ Running go test"
@GOFLAGS="$(GOFLAGS)" go test ./...
# Activate pre-commit hooks
pre-commit:
pre-commit install -t pre-commit -t commit-msg
# Deactivate pre-commit hooks
unpre-commit:
pre-commit uninstall -t pre-commit -t commit-msg
pre-commit-run:
@pre-commit run -a --hook-stage manual
# Install linters
lint-install:
@mkdir -p $(TMP_DIR)
@rm -rf $(TMP_DIR)/linters
@git -c advice.detachedHead=false clone --branch v$(TRUECLOUDLAB_LINT_VERSION) https://git.frostfs.info/TrueCloudLab/linters.git $(TMP_DIR)/linters
@make -C $(TMP_DIR)/linters lib CGO_ENABLED=1 OUT_DIR=$(OUTPUT_LINT_DIR)
@rm -rf $(TMP_DIR)/linters
@rmdir $(TMP_DIR) 2>/dev/null || true
@CGO_ENABLED=1 GOBIN=$(LINT_DIR) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v$(LINT_VERSION)
# Run linters
lint:
@if [ ! -d "$(LINT_DIR)" ]; then \
echo "Run make lint-install"; \
exit 1; \
fi
@$(LINT_DIR)/golangci-lint run
# Install staticcheck
staticcheck-install:
@go install honnef.co/go/tools/cmd/staticcheck@latest
# Run staticcheck
staticcheck-run:
@staticcheck ./...
easyjson-install:
@rm -rf $(EASYJSON_DIR)
@mkdir -p $(EASYJSON_DIR)
@GOBIN=$(EASYJSON_DIR) go install github.com/mailru/easyjson/...@$(EASYJSON_VERSION)
generate:
@if [ ! -d "$(EASYJSON_DIR)" ]; then \
make easyjson-install; \
fi
find ./ -name "_easyjson.go" -exec rm -rf {} \;
$(EASYJSON_DIR)/easyjson ./pkg/chain/chain.go

85
docs/ape.md Normal file
View file

@ -0,0 +1,85 @@
# Access policy engine
## General overview
### Purpose
Access policy engine (APE) is aimed at checking if a request can be performed over a resource by looking up the set chains of rules.
#### Terms
| Term | Description | Structure overview |
| -------------- | -------------------------------------------------------------- | -------------- |
| `Request` | The action that is being performed on the `Resource`. | <ul><li>`Operation` - `GetObject`,`PutObject` etc.;</li><li>`Properties` - actor's public key, actor's attributes;</li><li>`Resource`.</li></ul> |
| `Resource` | The object that the request is being performed on. Check also [resource.md](./resource.md). | <ul><li>`Name` - strictly formatted string value;</li><li>`Properties`.</li></ul> |
| `Chain` | A chain of `Rule`-s defined for a specific target. Chains are strictly distinguished by `Name`-s , i.e. chains with name `ingress` are not intersected with chains with name `s3`. Chains are stored in serialized format. | <ul><li>Base64-encoded `ID`;</li><li>List of `Rule`-s;</li><li>`MatchType` - defines rule status selection priority.</li></ul> |
| `Rule` | `Rule` defines which status is returned if `Request` matches all conditions. | <ul><li>`Status`: `Allow`, `AccessDenied`, `QuotaLimitReached`, `NoRuleFound`;</li><li>`Actions` - operation defined by a schema (`GetObject`, `PutContainer` etc.);</li><li>`Resources`;</li><li>`Any` - if `true` then `Reqeust` matches `Rule` if any `Condition` is `true`;</li><li>`Conditions`.</li></ul> |
| `Name` | `Name` of a chain (do not confuse with chain ID). `Name` defines a layer of `Chain`'s usage, so chains are distinguished by `Name`-s. Basically, `Name` refers to a protocol. | String value (`ingress`, `s3`, `iam`). |
| `Target` | A scope of request. `Target` can be either simple (only namespace; only container; only user; only groups) or compound (namespace + container). | <ul><li>`Namespace`;</li><li>`Container`;</li><li>`User`;</li><li>`Groups`.</li></ul> |
| `Engine` | `Engine` checks a request in a scope defined by `Target`. First, it is trying to match a request with rules defined in `LocalOverrideStorage` and, then, in `MorphRuleChainStorage`. | <ul><li>`LocalOverrideStorage` - chains stored in the local override storage have the highest priority</li><li>`MorphRuleChainStorage` - basically, chains stored in `Policy` contract;</li><li>`ChainRouter` - looks up chains and try to match them with `Request`.</li></ul> |
#### Details
Here some entities are overviewed in more detail.
##### Resource
`Resource`'s name is strictly formatted, the format is defined by a schema (`native`, `aws` etc.). Examples:
```bash
# The resource is the particular object with the address within Root namespace
native:object//HRwWbb1bJjRms33kkA21hy4JdPfARaH3fW9NfuNN6Fgj/EbxzAdz5LB4uqxuz6crWKAumBNtZyK2rKsqQP7TdZvwr
# The resource is all objects within the container within Root namespace
native:object//HRwWbb1bJjRms33kkA21hy4JdPfARaH3fW9NfuNN6Fgj/*
# The resource is the particular container within the namespace
native:container/namespace1/HRwWbb1bJjRms33kkA21hy4JdPfARaH3fW9NfuNN6Fgj
# The resource is all containers within the namespace
native:container/namespace1/*
```
##### Rule
`Rule` works out if:
1. a requests's operation matches the rule's `Actions`;
2. resource name matches the rule's `Resources`;
3. if all (or at least one if `Any=true`) conditions in `Condition` is met. Each condition defines how to retrieve
and compare the retrieved value. If `Condition`'s `Object` is set to `Resource` then the value is retrieved from the
resource's properties (example: container zone attribute). If `Object` is set to `Request`, the it's retrieved from the request's properties (example: actor's public key).
###### Name matching
`Resource`'s name in `Rule` may contain wildcard '*' that can be considered as a regular expression:
```bash
# The resource is all objects within the container within Root namespace
native:object//HRwWbb1bJjRms33kkA21hy4JdPfARaH3fW9NfuNN6Fgj/*
```
If an incoming request has such a resource name, then names are matched:
```bash
# The resource is all objects within the container within Root namespace
native:object//HRwWbb1bJjRms33kkA21hy4JdPfARaH3fW9NfuNN6Fgj/EbxzAdz5LB4uqxuz6crWKAumBNtZyK2rKsqQP7TdZvwr
```
If the incoming request has such a resource name that specifies a container's object within namespace, for instance, `namespicy`,
then matching does not work out:
```bash
# The resource is all objects within the container within `namespicy` namespace:
native:object/namespicy/HRwWbb1bJjRms33kkA21hy4JdPfARaH3fW9NfuNN6Fgj/EbxzAdz5LB4uqxuz6crWKAumBNtZyK2rKsqQP7TdZvwr
```
##### Engine
`Engine` is trying to match the request against **the target** looking up chain rules, firstly, in `LocalOverrideStorage` (these rules are also known as *local overrides*) and then in `MorphRuleChainStorage` (contract `Policy`). Both storages iterate chain rules according to the specified priority of the targets: `namespace` -> `container` -> `user` -> `groups`.
#### Diagrams
The diagram demonstrates a scenario in Storage node. The request `A` cannot be performed as APE matched
the request and returned `Access Denied` status. The request `B` is allowed and the client gets `OK` status.
![Storage node](images/ape/storage_node_ape.svg)
The diagram demonstrates a complex scenario with S3, IAM and Storage node.
![S3 and IAM](images/ape/s3_ape.svg)

View file

@ -0,0 +1,61 @@
@startuml s3 ape
participant "Client" as client
participant "IAM" as iam
participant "IAM -> APE converter" as converter
box "S3" #HotPink
participant "S3 gateway" as s3
end box
box "Access Policy Engine (as s3 middleware)" #LightPink
participant "Local override storage" as s3localOverrides
participant "Chain router" as s3chainRouter
end box
box "Policy contract (shared)"
participant "Morph rule storage" as morphRuleStorage
end box
box "Access Policy Engine (as storage middleware)" #LightGreen
participant "Chain Router" as storageChainRouter
participant "Local override storage" as storageLocalOverrides
end box
box "Storage node" #Green
participant "Object service" as obj
participant "Control service" as control
end box
group Request IAM to set a policy
client -> iam : Set IAM policy
iam -> converter : Convert IAM policy
converter -> iam : Return APE chain
iam -> morphRuleStorage : Store IAM policy and APE chain
iam -> s3localOverrides : Set S3 local overrides
iam -> client : OK
end
group Request S3 to set a policy
client -> s3 : Set bucket policy
s3 -> converter : Convert IAM policy
converter -> s3 : Return APE chain
s3 -> morphRuleStorage : Store bucket policy and APE chain
s3 -> client : OK
end
group Get object
client -> s3: GetObject
s3 -> s3chainRouter: Check if APE allows request for S3
note over s3chainRouter: matching the request with overrides and rules
s3chainRouter -> s3: Status: ALLOW
s3 -> obj: Get object
obj -> storageChainRouter: Check if APE allows the request
note over storageChainRouter : matching the request with overrides and rules
storageChainRouter -> obj: Status: ALLOW
obj -> s3: Response: OK, Object
s3 -> client: Response: OK, Object
end
@enduml

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,46 @@
@startuml storage node ape
!pragma teoz true
participant "Administrator" as administrator
participant "Client" as client
box "Storage node" #Green
participant "Object service" as obj
participant "Control service" as control
end box
box "Access Policy Engine" #LightGreen
participant "Local override storage" as localOverrides
participant "Chain Router" as chainRouter
participant "Morph rule storage" as morphRuleStorage
end box
group Set local override
client -> control: Add local override
control -> localOverrides: Save override in DB
localOverrides -> control: OK
control -> client: OK
end
group Update state in Policy contract
administrator -> morphRuleStorage: Add chain
morphRuleStorage -> administrator: OK
end
group Perform a request A
client -> obj : Sending a request
obj -> chainRouter: Check if APE allows the request
note over chainRouter : Fetches local overrides and rules defined for a target/targets and looks for a match
chainRouter -> obj: APE returns status: "ACCESS DENIED"
obj -> client: Response: "the request is denied"
end
group Perform a request B
client -> obj : Sending a request
obj -> chainRouter: Check if APE allows the request
note over chainRouter : Fetches local overrides and rules defined for a target/targets and looks for a match
chainRouter -> obj: APE returns status: "ALLOW"
obj -> client: Response: "OK"
end
@enduml

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

439
docs/policy_converters.md Normal file
View file

@ -0,0 +1,439 @@
# Policy converters
This repository contains converters that provide opportunities to
transform [AWS IAM policies](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies.html) to inner
FrostFS policy format. This document describes such transformations.
## FrostFS
As it was mentioned there are converters that transform AWS IAM policies to FrostFS.
Here common examples of AWS:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": "*"
}
]
}
```
and FrostFS:
```json
{
"ID": "c29tZS1pZA==",
"Rules": [
{
"Status": "Allow",
"Actions": {
"Inverted": false,
"Names": [
"s3:*"
]
},
"Resources": {
"Inverted": false,
"Names": [
"*"
]
},
"Any": false,
"Condition": null
}
],
"MatchType": "DenyPriority"
}
```
policies.
Despite there is only one FrostFS format, we have two converters (`s3` and `native`). The reason is S3 gateway and
Storage node have different actions and resource naming:
* S3 has [a lot of methods](https://docs.aws.amazon.com/AmazonS3/latest/API/API_Operations.html) and operates with
bucket/object
* Storage node has only 6 container and 7 object methods and operates container/object (that has different format)
The following sections describe each transformation more precisely ([common](#common) sections contains shared concepts)
### Common
#### Fields
Rough json main fields mapping:
| AWS policy field | FrostFS policy field | Comment |
|------------------|----------------------|------------------------------------------------------------------|
| `Version` | - | Not applicable |
| `Statement` | `Rules` | |
| `Effect` | `Status` | |
| `Action` | `Actions.Names` | `Actions.Inverted` = false |
| `NotAction` | `Actions.Names` | `Actions.Inverted` = true |
| `Resource` | `Resources.Names` | `Resources.Inverted` = false |
| `NotResource` | `Resources.Names` | `Resources.Inverted` = true |
| `Condition` | `Condition` | `Any` = false, that means the conditions must be hold altogether |
| `Principal` | - | Expressed via conditions (depends on s3/native converters) |
### Conditions
Each condition in FrostFS policy can add requirements to some request/resource properties
and consists of the following fields:
| Field | Description |
|----------|-------------------------------------------------------------------------------------------|
| `Op` | Condition type operation (`StringEqual`, `NumericEqual` etc) |
| `Object` | Property type to which condition can be applied (`Request` property, `Resource` property) |
| `Key` | Property key |
| `Value` | Property value |
Conditions operators:
| AWS conditions operator | FrostFS condition operator | Comment |
|-----------------------------|-----------------------------|-------------------------------------------------------------------|
| `StringEquals` | `StringEquals` | |
| `StringNotEquals` | `StringNotEquals` | |
| `StringEqualsIgnoreCase` | `StringEqualsIgnoreCase` | |
| `StringNotEqualsIgnoreCase` | `StringNotEqualsIgnoreCase` | |
| `StringLike` | `StringLike` | |
| `StringNotLike` | `StringNotLike` | |
| `NumericEquals` | `NumericEquals` | |
| `NumericNotEquals` | `NumericNotEquals` | |
| `NumericLessThan` | `NumericLessThan` | |
| `NumericLessThanEquals` | `NumericLessThanEquals` | |
| `NumericGreaterThan` | `NumericGreaterThan` | |
| `NumericGreaterThanEquals` | `NumericGreaterThanEquals` | |
| `DateEquals` | `StringEquals` | Date transforms to unix timestamp to be compared as string |
| `DateNotEquals` | `StringNotEquals` | Date transforms to unix timestamp to be compared as string |
| `DateLessThan` | `StringEqualsIgnoreCase` | Date transforms to unix timestamp to be compared as string |
| `DateLessThanEquals` | `StringNotEqualsIgnoreCase` | Date transforms to unix timestamp to be compared as string |
| `DateGreaterThan` | `StringLike` | Date transforms to unix timestamp to be compared as string |
| `DateGreaterThanEquals` | `StringNotLike` | Date transforms to unix timestamp to be compared as string |
| `Bool` | `StringEqualsIgnoreCase` | |
| `IpAddress` | `IPAddress` | |
| `NotIpAddress` | `NotIPAddress` | |
| `ArnEquals` | `StringEquals` | |
| `ArnLike` | `StringLike` | |
| `ArnNotEquals` | `StringNotEquals` | |
| `ArnNotLike` | `StringNotLike` | |
| `SliceContains` | `SliceContains` | AWS spec doesn't contain such operator. This is FrostFS extension |
For example, AWS conditions:
```json
{
"Condition": {
"ArnEquals": {"key16": ["val16"]},
"ArnNotEquals": {"key18": ["val18"]},
"ArnNotLike": {"key19": ["val19"]},
"Bool": {"key13": ["True"]},
"DateEquals": {"key7": ["2006-01-02T15:04:05+07:00"]},
"DateGreaterThan": {"key11": ["2006-01-02T15:04:05-01:00"]},
"DateGreaterThanEquals": {"key12": ["2006-01-02T15:04:05-03:00"]},
"DateLessThan": {"key9": ["2006-01-02T15:04:05+06:00"]},
"DateLessThanEquals": {"key10": ["2006-01-02T15:04:05+03:00"]},
"DateNotEquals": {"key8": ["2006-01-02T15:04:05Z"]},
"NumericEquals": {"key20": ["-20"]},
"NumericGreaterThan": {"key24": ["-24.24"]},
"NumericGreaterThanEquals": {"key25": ["+25.25"]},
"NumericLessThan": {"key22": ["0"]},
"NumericLessThanEquals": {"key23": ["23.23"]},
"NumericNotEquals": {"key21": ["+21"]},
"StringEquals": {"key1": ["val0"]},
"StringEqualsIgnoreCase": {"key3": ["val3"]},
"StringLike": {"key5": ["val5"]},
"StringNotEquals": {"key2": ["val2"]},
"StringNotEqualsIgnoreCase": {"key4": ["val4"]},
"StringNotLike": {"key6": ["val6"]}
}
}
```
transforms to FrostFS conditions:
```json
{
"Condition": [
{"Op": "StringLike", "Object": "Request", "Key": "key5", "Value": "val5"},
{"Op": "StringNotEquals", "Object": "Request", "Key": "key2", "Value": "val2"},
{"Op": "StringGreaterThan", "Object": "Request", "Key": "key11", "Value": "1136217845"},
{"Op": "StringGreaterThanEquals", "Object": "Request", "Key": "key12", "Value": "1136225045"},
{"Op": "StringLessThan", "Object": "Request", "Key": "key9", "Value": "1136192645"},
{"Op": "StringEqualsIgnoreCase", "Object": "Request", "Key": "key3", "Value": "val3"},
{"Op": "StringEquals", "Object": "Request", "Key": "key16", "Value": "val16"},
{"Op": "NumericLessThanEquals", "Object": "Request", "Key": "key23", "Value": "23.23"},
{"Op": "StringNotEqualsIgnoreCase", "Object": "Request", "Key": "key4", "Value": "val4"},
{"Op": "StringEquals", "Object": "Request", "Key": "key1", "Value": "val0"},
{"Op": "StringLessThanEquals", "Object": "Request", "Key": "key10", "Value": "1136203445"},
{"Op": "NumericGreaterThan", "Object": "Request", "Key": "key24", "Value": "-24.24"},
{"Op": "NumericGreaterThanEquals", "Object": "Request", "Key": "key25", "Value": "+25.25"},
{"Op": "NumericLessThan", "Object": "Request", "Key": "key22", "Value": "0"},
{"Op": "StringNotEquals", "Object": "Request", "Key": "key8", "Value": "1136214245"},
{"Op": "NumericEquals", "Object": "Request", "Key": "key20", "Value": "-20"},
{"Op": "NumericNotEquals", "Object": "Request", "Key": "key21", "Value": "+21"},
{"Op": "StringNotLike", "Object": "Request", "Key": "key6", "Value": "val6"},
{"Op": "StringNotEquals", "Object": "Request", "Key": "key18", "Value": "val18"},
{"Op": "StringNotLike", "Object": "Request", "Key": "key19", "Value": "val19"},
{"Op": "StringEqualsIgnoreCase", "Object": "Request", "Key": "key13", "Value": "True"},
{"Op": "StringEquals", "Object": "Request", "Key": "key7", "Value": "1136189045"}
]
}
```
### S3
#### Actions
Each action allows some s3-gw methods, so we must transform action to specific method names
(you can see exact mapping in table in [this file](../iam/converter_s3.go)).
For example the following actions:
```json
{
"Action": [
"s3:DeleteObject",
"iam:CreateUser"
]
}
```
transforms to
```json
{
"Actions": {
"Inverted": false,
"Names": [
"s3:DeleteObject",
"s3:DeleteMultipleObjects",
"iam:CreateUser"
]
}
}
```
As we can see any `iam:*` action transformed as it is. But `s3:*` actions transforms according to
[spec rules](https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazons3.html) and s3-gw
[method names](https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/src/commit/2ab655b909c40db6f7a4e41e07d8b99167f791bd/api/middleware/constants.go#L3-L76).
#### Resources
Resource is transformed as it is:
```json
{
"Resource": [
"arn:aws:s3:::bucket/object"
]
}
```
```json
{
"Resources": {
"Inverted": false,
"Names": [
"arn:aws:s3:::bucket/object"
]
}
}
```
#### Principals
To check user s3-gw uses special condition request property (`Owner`), so when AWS policy contains principal field
it transforms to rule with appropriate condition. To get correct `Owner` property value special user resolver
(`S3Resolver` interface in [converter_s3 file](../iam/converter_s3.go)) must be provided into convert function.
For example such AWS json statement:
```json
{
"Effect": "Allow",
"Action": "*",
"Resource": "*",
"Principal": {
"AWS": "arn:aws:iam::111122223333:user/JohnDoe"
}
}
```
transforms to the following FrostFS rule:
```json
{
"Status": "Allow",
"Actions": {
"Inverted": false,
"Names": [
"*"
]
},
"Resources": {
"Inverted": false,
"Names": [
"*"
]
},
"Any": false,
"Condition": [
{
"Op": "StringEquals",
"Object": "Request",
"Key": "Owner",
"Value": "NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM"
}
]
}
```
### Native
#### Actions
Each action allows some frostfs methods, so we must transform action to specific method names
(you can see exact mapping in table in [this file](../iam/converter_native.go)).
For example the following actions:
```json
{
"Action": [
"s3:DeleteObject",
"iam:CreateUser"
]
}
```
transforms to
```json
{
"Actions": {
"Inverted": false,
"Names": [
"PutObject",
"HeadObject",
"GetObject",
"RangeObject",
"GetContainer",
"DeleteObject"
]
}
}
```
> **Note:** Only subset of s3:* actions can be transformed (exact value you can see in mapping table mentioned before).
> If all provided actions is not applicable converter function returns appropriate error.
Native methods (to which original actions are transformed) depend on which methods are invoked by appropriate s3-gw
[method](https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/src/commit/2ab655b909c40db6f7a4e41e07d8b99167f791bd/api/middleware/constants.go#L3-L76).
So in example above s3-gw during performing DeleteObject methods invokes the following methods:
`["PutObject","HeadObject","GetObject","RangeObject","GetContainer","DeleteObject"]`
#### Resources
To transform resources the following is being performed:
* Bucket name is resoled to container id (by providing `NativeResolver` interface implementation to converter)
* Object name is transformed to condition with special `FilePath` attribute
(that present on every object that was uploaded via s3-gw)
For example, the following AWS policy statement:
```json
{
"Principal": "*",
"Effect": "Allow",
"Action": "*",
"Resource": "arn:aws:s3:::bucket/object"
}
```
transforms to FrostFS native policy rule:
```json
{
"Status": "Allow",
"Actions": {
"Inverted": false,
"Names": [
"*"
]
},
"Resources": {
"Inverted": false,
"Names": [
"native:object//bucket/HFq67qbfhFEiEL7qDXqayo3F78yAvxXSXzwSa2hKM9bH/*",
"native:container//bucket/HFq67qbfhFEiEL7qDXqayo3F78yAvxXSXzwSa2hKM9bH"
]
},
"Any": false,
"Condition": [
{
"Op": "StringLike",
"Object": "Resource",
"Key": "FilePath",
"Value": "object"
}
]
}
```
#### Principals
To check user s3-gw uses special condition request property (`$Actor:publicKey`), so when AWS policy contains principal
field it transforms to rule with appropriate condition. To get correct `$Actor:publicKey` property value
special user resolver (`NativeResolver` interface in [converter_native file](../iam/converter_native.go)) must be
provided into convert function.
For example such AWS json statement:
```json
{
"Effect": "Allow",
"Action": "*",
"Resource": "*",
"Principal": {
"AWS": "arn:aws:iam::111122223333:user/JohnDoe"
}
}
```
transforms to the following FrostFS rule:
```json
{
"Status": "Allow",
"Actions": {
"Inverted": false,
"Names": [
"*"
]
},
"Resources": {
"Inverted": false,
"Names": [
"native:object/*",
"native:container/*"
]
},
"Any": false,
"Condition": [
{
"Op": "StringEquals",
"Object": "Request",
"Key": "$Actor:publicKey",
"Value": "031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a"
}
]
}
```

20
docs/resource.md Normal file
View file

@ -0,0 +1,20 @@
# Resource
From the point of the access policy engine, a resource is an object to which a request is being performed.
This can be an object in a container within a namespace, or all objects in a container,
or all containers within the root namespace etc.
A resource can be viewed from two sides:
- As part of a [request](../pkg/resource/resource.go). In this case a resource has a name and properties.
- As part of rule [chain](../pkg/chain/chain.go): a resource has just a name.
## Resource name
A resource name must have a such format that can be processed by a chain router that matches a request
either with local overrides or with rules within policy contract to get if this request is allowed to be performed.
The main idea of this format is for the chain router to match by full name (`native:object//cnrID/objID`) or
wildcard (`native:object//cnrID/*`).
Check out formats that are defined in the schema: [native formats](../schema/native/consts.go), [s3 formats](../schema/s3/consts.go).
You should validate a resource name using [util](../schema/native/util/validation.go) before instantiating a request or
before putting it to either to local override storage or the policy contract storage.

35
go.mod Normal file
View file

@ -0,0 +1,35 @@
module git.frostfs.info/TrueCloudLab/policy-engine
go 1.22
require (
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409111539-e7a05a49ff45
github.com/google/uuid v1.3.1
github.com/mailru/easyjson v0.7.7
github.com/nspcc-dev/neo-go v0.105.0
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/hashicorp/golang-lru v0.6.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/nspcc-dev/go-ordered-json v0.0.0-20231123160306-3374ff1e7a3c // indirect
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231127165613-b35f351f0ba0 // indirect
github.com/nspcc-dev/rfc6979 v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 // indirect
github.com/twmb/murmur3 v1.1.5 // indirect
go.etcd.io/bbolt v1.3.8 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

166
go.sum Normal file
View file

@ -0,0 +1,166 @@
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=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM=
github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/nspcc-dev/go-ordered-json v0.0.0-20231123160306-3374ff1e7a3c h1:OOQeE613BH93ICPq3eke5N78gWNeMjcBWkmD2NKyXVg=
github.com/nspcc-dev/go-ordered-json v0.0.0-20231123160306-3374ff1e7a3c/go.mod h1:79bEUDEviBHJMFV6Iq6in57FEOCMcRhfQnfaf0ETA5U=
github.com/nspcc-dev/neo-go v0.105.0 h1:vtNZYFEFySK8zRDhLzQYha849VzWrcKezlnq/oNQg/w=
github.com/nspcc-dev/neo-go v0.105.0/go.mod h1:6pchIHg5okeZO955RxpTh5q0sUI0vtpgPM6Q+no1rlI=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231127165613-b35f351f0ba0 h1:N+dMIBmteXjJpkH6UZ7HmNftuFxkqszfGLbhsEctnv0=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231127165613-b35f351f0ba0/go.mod h1:J/Mk6+nKeKSW4wygkZQFLQ6SkLOSGX5Ga0RuuuktEag=
github.com/nspcc-dev/rfc6979 v0.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE=
github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs=
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM=
github.com/twmb/murmur3 v1.1.5 h1:i9OLS9fkuLzBXjt6dptlAEyk58fJsSTXbRg3SgVyqgk=
github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo=
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c=
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E=
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=

425
iam/converter.go Normal file
View file

@ -0,0 +1,425 @@
package iam
import (
"errors"
"fmt"
"net/netip"
"strconv"
"strings"
"time"
"unicode/utf8"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/common"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
)
const (
s3ActionAbortMultipartUpload = "s3:AbortMultipartUpload"
s3ActionCreateBucket = "s3:CreateBucket"
s3ActionDeleteBucket = "s3:DeleteBucket"
s3ActionDeleteBucketPolicy = "s3:DeleteBucketPolicy"
s3ActionDeleteObject = "s3:DeleteObject"
s3ActionDeleteObjectTagging = "s3:DeleteObjectTagging"
s3ActionDeleteObjectVersion = "s3:DeleteObjectVersion"
s3ActionDeleteObjectVersionTagging = "s3:DeleteObjectVersionTagging"
s3ActionGetBucketACL = "s3:GetBucketAcl"
s3ActionGetBucketCORS = "s3:GetBucketCORS"
s3ActionGetBucketLocation = "s3:GetBucketLocation"
s3ActionGetBucketNotification = "s3:GetBucketNotification"
s3ActionGetBucketObjectLockConfiguration = "s3:GetBucketObjectLockConfiguration"
s3ActionGetBucketPolicy = "s3:GetBucketPolicy"
s3ActionGetBucketPolicyStatus = "s3:GetBucketPolicyStatus"
s3ActionGetBucketTagging = "s3:GetBucketTagging"
s3ActionGetBucketVersioning = "s3:GetBucketVersioning"
s3ActionGetLifecycleConfiguration = "s3:GetLifecycleConfiguration"
s3ActionGetObject = "s3:GetObject"
s3ActionGetObjectACL = "s3:GetObjectAcl"
s3ActionGetObjectAttributes = "s3:GetObjectAttributes"
s3ActionGetObjectLegalHold = "s3:GetObjectLegalHold"
s3ActionGetObjectRetention = "s3:GetObjectRetention"
s3ActionGetObjectTagging = "s3:GetObjectTagging"
s3ActionGetObjectVersion = "s3:GetObjectVersion"
s3ActionGetObjectVersionACL = "s3:GetObjectVersionAcl"
s3ActionGetObjectVersionAttributes = "s3:GetObjectVersionAttributes"
s3ActionGetObjectVersionTagging = "s3:GetObjectVersionTagging"
s3ActionListAllMyBuckets = "s3:ListAllMyBuckets"
s3ActionListBucket = "s3:ListBucket"
s3ActionListBucketMultipartUploads = "s3:ListBucketMultipartUploads"
s3ActionListBucketVersions = "s3:ListBucketVersions"
s3ActionListMultipartUploadParts = "s3:ListMultipartUploadParts"
s3ActionPutBucketACL = "s3:PutBucketAcl"
s3ActionPutBucketCORS = "s3:PutBucketCORS"
s3ActionPutBucketNotification = "s3:PutBucketNotification"
s3ActionPutBucketObjectLockConfiguration = "s3:PutBucketObjectLockConfiguration"
s3ActionPutBucketPolicy = "s3:PutBucketPolicy"
s3ActionPutBucketTagging = "s3:PutBucketTagging"
s3ActionPutBucketVersioning = "s3:PutBucketVersioning"
s3ActionPutLifecycleConfiguration = "s3:PutLifecycleConfiguration"
s3ActionPutObject = "s3:PutObject"
s3ActionPutObjectACL = "s3:PutObjectAcl"
s3ActionPutObjectLegalHold = "s3:PutObjectLegalHold"
s3ActionPutObjectRetention = "s3:PutObjectRetention"
s3ActionPutObjectTagging = "s3:PutObjectTagging"
s3ActionPutObjectVersionACL = "s3:PutObjectVersionAcl"
s3ActionPutObjectVersionTagging = "s3:PutObjectVersionTagging"
s3ActionPatchObject = "s3:PatchObject"
)
const (
condKeyAWSPrincipalARN = "aws:PrincipalArn"
condKeyAWSSourceIP = "aws:SourceIp"
condKeyAWSPrincipalTagPrefix = "aws:PrincipalTag/"
condKeyAWSRequestTagPrefix = "aws:RequestTag/"
condKeyAWSResourceTagPrefix = "aws:ResourceTag/"
userClaimTagPrefix = "tag-"
)
const (
// String condition operators.
CondStringEquals string = "StringEquals"
CondStringNotEquals string = "StringNotEquals"
CondStringEqualsIgnoreCase string = "StringEqualsIgnoreCase"
CondStringNotEqualsIgnoreCase string = "StringNotEqualsIgnoreCase"
CondStringLike string = "StringLike"
CondStringNotLike string = "StringNotLike"
// Numeric condition operators.
CondNumericEquals string = "NumericEquals"
CondNumericNotEquals string = "NumericNotEquals"
CondNumericLessThan string = "NumericLessThan"
CondNumericLessThanEquals string = "NumericLessThanEquals"
CondNumericGreaterThan string = "NumericGreaterThan"
CondNumericGreaterThanEquals string = "NumericGreaterThanEquals"
// Date condition operators.
CondDateEquals string = "DateEquals"
CondDateNotEquals string = "DateNotEquals"
CondDateLessThan string = "DateLessThan"
CondDateLessThanEquals string = "DateLessThanEquals"
CondDateGreaterThan string = "DateGreaterThan"
CondDateGreaterThanEquals string = "DateGreaterThanEquals"
// Bolean condition operators.
CondBool string = "Bool"
// IP address condition operators.
CondIPAddress string = "IpAddress"
CondNotIPAddress string = "NotIpAddress"
// ARN condition operators.
CondArnEquals string = "ArnEquals"
CondArnLike string = "ArnLike"
CondArnNotEquals string = "ArnNotEquals"
CondArnNotLike string = "ArnNotLike"
// Custom condition operators.
CondSliceContains string = "SliceContains"
)
const (
arnIAMPrefix = "arn:aws:iam::"
s3ResourcePrefix = "arn:aws:s3:::"
s3ActionPrefix = "s3:"
iamActionPrefix = "iam:"
)
var (
// ErrInvalidPrincipalFormat occurs when principal has unknown/unsupported format.
ErrInvalidPrincipalFormat = errors.New("invalid principal format")
// ErrInvalidResourceFormat occurs when resource has unknown/unsupported format.
ErrInvalidResourceFormat = errors.New("invalid resource format")
// ErrInvalidActionFormat occurs when action has unknown/unsupported format.
ErrInvalidActionFormat = errors.New("invalid action format")
// ErrActionsNotApplicable occurs when failed to convert any actions.
ErrActionsNotApplicable = errors.New("actions not applicable")
)
type formPrincipalConditionFunc func(string) chain.Condition
type transformConditionFunc func(gr GroupedConditions) (GroupedConditions, error)
func convertToChainConditions(c Conditions, transformer transformConditionFunc) ([]GroupedConditions, error) {
conditions, err := convertToChainCondition(c)
if err != nil {
return nil, err
}
for i := range conditions {
if conditions[i], err = transformer(conditions[i]); err != nil {
return nil, fmt.Errorf("transform condition: %w", err)
}
}
return conditions, nil
}
type GroupedConditions struct {
Conditions []chain.Condition
Any bool
}
func convertToChainCondition(c Conditions) ([]GroupedConditions, error) {
var grouped []GroupedConditions
for op, KVs := range c {
condType, convertValue, err := getConditionTypeAndConverter(op)
if err != nil {
return nil, err
}
for key, values := range KVs {
group := GroupedConditions{
Conditions: make([]chain.Condition, len(values)),
Any: len(values) > 1,
}
for i, val := range values {
converted, err := convertValue(val)
if err != nil {
return nil, err
}
group.Conditions[i] = chain.Condition{
Op: condType,
Kind: chain.KindRequest,
Key: transformKey(key),
Value: converted,
}
}
grouped = append(grouped, group)
}
}
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"):
switch op {
case CondStringEquals:
return chain.CondStringEquals, noConvertFunction, nil
case CondStringNotEquals:
return chain.CondStringNotEquals, noConvertFunction, nil
case CondStringEqualsIgnoreCase:
return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil
case CondStringNotEqualsIgnoreCase:
return chain.CondStringNotEqualsIgnoreCase, noConvertFunction, nil
case CondStringLike:
return chain.CondStringLike, noConvertFunction, nil
case CondStringNotLike:
return chain.CondStringNotLike, noConvertFunction, nil
default:
return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op)
}
case strings.HasPrefix(op, "Arn"):
switch op {
case CondArnEquals:
return chain.CondStringEquals, noConvertFunction, nil
case CondArnNotEquals:
return chain.CondStringNotEquals, noConvertFunction, nil
case CondArnLike:
return chain.CondStringLike, noConvertFunction, nil
case CondArnNotLike:
return chain.CondStringNotLike, noConvertFunction, nil
default:
return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op)
}
case strings.HasPrefix(op, "Numeric"):
return numericConditionTypeAndConverter(op)
case strings.HasPrefix(op, "Date"):
switch op {
case CondDateEquals:
return chain.CondStringEquals, dateConvertFunction, nil
case CondDateNotEquals:
return chain.CondStringNotEquals, dateConvertFunction, nil
case CondDateLessThan:
return chain.CondStringLessThan, dateConvertFunction, nil
case CondDateLessThanEquals:
return chain.CondStringLessThanEquals, dateConvertFunction, nil
case CondDateGreaterThan:
return chain.CondStringGreaterThan, dateConvertFunction, nil
case CondDateGreaterThanEquals:
return chain.CondStringGreaterThanEquals, dateConvertFunction, nil
default:
return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op)
}
case op == CondBool:
return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil
case op == CondIPAddress:
return chain.CondIPAddress, ipConvertFunction, nil
case op == CondNotIPAddress:
return chain.CondNotIPAddress, ipConvertFunction, nil
case op == CondSliceContains:
return chain.CondSliceContains, noConvertFunction, nil
default:
return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op)
}
}
func numericConditionTypeAndConverter(op string) (chain.ConditionType, convertFunction, error) {
switch op {
case CondNumericEquals:
return chain.CondNumericEquals, numericConvertFunction, nil
case CondNumericNotEquals:
return chain.CondNumericNotEquals, numericConvertFunction, nil
case CondNumericLessThan:
return chain.CondNumericLessThan, numericConvertFunction, nil
case CondNumericLessThanEquals:
return chain.CondNumericLessThanEquals, numericConvertFunction, nil
case CondNumericGreaterThan:
return chain.CondNumericGreaterThan, numericConvertFunction, nil
case CondNumericGreaterThanEquals:
return chain.CondNumericGreaterThanEquals, numericConvertFunction, nil
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
}
parsed, err := time.Parse(time.RFC3339, val)
if err != nil {
return "", err
}
return strconv.FormatInt(parsed.UTC().Unix(), 10), nil
}
func parsePrincipalAsIAMUser(principal string) (account string, user string, err error) {
if !strings.HasPrefix(principal, arnIAMPrefix) {
return "", "", ErrInvalidPrincipalFormat
}
// iam arn format arn:aws:iam::<account>:user/<user-name-with-path>
iamResource := strings.TrimPrefix(principal, arnIAMPrefix)
sepIndex := strings.Index(iamResource, ":user/")
if sepIndex < 0 {
return "", "", ErrInvalidPrincipalFormat
}
account = iamResource[:sepIndex]
user = iamResource[sepIndex+6:]
if len(user) == 0 {
return "", "", ErrInvalidPrincipalFormat
}
userNameIndex := strings.LastIndexByte(user, '/')
if userNameIndex > -1 {
user = user[userNameIndex+1:]
if len(user) == 0 {
return "", "", ErrInvalidPrincipalFormat
}
}
return account, user, nil
}
func validateResource(resource string) error {
if resource == Wildcard {
return nil
}
if !strings.HasPrefix(resource, s3ResourcePrefix) && !strings.HasPrefix(resource, arnIAMPrefix) {
return ErrInvalidResourceFormat
}
index := strings.IndexByte(resource, Wildcard[0])
if index != -1 && index != utf8.RuneCountInString(resource)-1 {
return ErrInvalidResourceFormat
}
return nil
}
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 false, ErrInvalidActionFormat
}
return isIAM, nil
}
func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition {
var orConditions []chain.Condition
commonConditions := make([]chain.Condition, 0, len(groupedConditions))
for _, grouped := range groupedConditions {
if grouped.Any {
orConditions = append(orConditions, grouped.Conditions...)
} else {
commonConditions = append(commonConditions, grouped.Conditions...)
}
}
if len(orConditions) == 0 {
return [][]chain.Condition{commonConditions}
}
res := make([][]chain.Condition, len(orConditions))
for i, condition := range orConditions {
res[i] = append([]chain.Condition{condition}, commonConditions...)
}
return res
}
func formStatus(statement Statement) chain.Status {
status := chain.AccessDenied
if statement.Effect == AllowEffect {
status = chain.Allow
}
return status
}

427
iam/converter_native.go Normal file
View file

@ -0,0 +1,427 @@
package iam
import (
"errors"
"fmt"
"strings"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
)
const PropertyKeyFilePath = "FilePath"
var actionToNativeOpMap = map[string][]string{
s3ActionAbortMultipartUpload: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
s3ActionCreateBucket: {native.MethodGetContainer, native.MethodPutContainer, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodPutObject},
s3ActionDeleteBucket: {native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject, native.MethodGetObject},
s3ActionDeleteBucketPolicy: {native.MethodGetContainer},
s3ActionDeleteObject: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject},
s3ActionDeleteObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
s3ActionDeleteObjectVersion: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject},
s3ActionDeleteObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
s3ActionGetBucketACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject},
s3ActionGetBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
s3ActionGetBucketLocation: {native.MethodGetContainer},
s3ActionGetBucketNotification: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
s3ActionGetBucketObjectLockConfiguration: {native.MethodGetContainer, native.MethodGetObject},
s3ActionGetBucketPolicy: {native.MethodGetContainer},
s3ActionGetBucketPolicyStatus: {native.MethodGetContainer},
s3ActionGetBucketTagging: {native.MethodGetContainer, native.MethodGetObject},
s3ActionGetBucketVersioning: {native.MethodGetContainer, native.MethodGetObject},
s3ActionGetLifecycleConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
s3ActionGetObject: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
s3ActionGetObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
s3ActionGetObjectAttributes: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
s3ActionGetObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject},
s3ActionGetObjectRetention: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject},
s3ActionGetObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject},
s3ActionGetObjectVersion: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
s3ActionGetObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
s3ActionGetObjectVersionAttributes: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
s3ActionGetObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject},
s3ActionListAllMyBuckets: {native.MethodListContainers, native.MethodGetContainer},
s3ActionListBucket: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
s3ActionListBucketMultipartUploads: {native.MethodGetContainer, native.MethodGetObject},
s3ActionListBucketVersions: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
s3ActionListMultipartUploadParts: {native.MethodGetContainer, native.MethodGetObject},
s3ActionPutBucketACL: {native.MethodGetContainer, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodPutObject},
s3ActionPutBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject},
s3ActionPutBucketNotification: {native.MethodGetContainer, native.MethodHeadObject, native.MethodDeleteObject, native.MethodGetObject, native.MethodPutObject},
s3ActionPutBucketObjectLockConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject},
s3ActionPutBucketPolicy: {native.MethodGetContainer},
s3ActionPutBucketTagging: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject},
s3ActionPutBucketVersioning: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject},
s3ActionPutLifecycleConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodPutObject, native.MethodDeleteObject},
s3ActionPutObject: {native.MethodGetContainer, native.MethodPutObject, native.MethodGetObject, native.MethodHeadObject, native.MethodRangeObject},
s3ActionPutObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
s3ActionPutObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
s3ActionPutObjectRetention: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
s3ActionPutObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
s3ActionPutObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
s3ActionPutObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
s3ActionPatchObject: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodPatchObject, native.MethodPutObject, native.MethodRangeObject},
}
var containerNativeOperations = map[string]struct{}{
native.MethodPutContainer: {},
native.MethodDeleteContainer: {},
native.MethodGetContainer: {},
native.MethodListContainers: {},
native.MethodSetContainerEACL: {},
native.MethodGetContainerEACL: {},
}
var objectNativeOperations = map[string]struct{}{
native.MethodGetObject: {},
native.MethodPutObject: {},
native.MethodHeadObject: {},
native.MethodDeleteObject: {},
native.MethodSearchObject: {},
native.MethodRangeObject: {},
native.MethodHashObject: {},
}
var errConditionKeyNotApplicable = errors.New("condition key is not applicable")
type NativeResolver interface {
GetUserKey(account, name string) (string, error)
GetBucketInfo(bucket string) (*BucketInfo, error)
}
type BucketInfo struct {
Namespace string
Container string
}
func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, error) {
if err := p.Validate(ResourceBasedPolicyType); err != nil {
return nil, err
}
var engineChain chain.Chain
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)
if err != nil {
return nil, err
}
ruleAction := chain.Actions{Inverted: actionInverted, Names: nativeActions}
if len(ruleAction.Names) == 0 {
continue
}
resource, resourceInverted := statement.resource()
groupedResources, err := formNativeResourceNamesAndConditions(resource, resolver, getActionTypes(nativeActions))
if err != nil {
return nil, err
}
groupedConditions, err := convertToNativeChainCondition(statement.Conditions, resolver)
if err != nil {
if errors.Is(err, errConditionKeyNotApplicable) {
continue
}
return nil, err
}
splitConditions := splitGroupedConditions(groupedConditions)
principals, principalCondFn, err := getNativePrincipalsAndConditionFunc(statement, resolver)
if err != nil {
return nil, err
}
for _, groupedResource := range groupedResources {
for _, principal := range principals {
for _, conditions := range splitConditions {
var principalCondition []chain.Condition
if principal != Wildcard {
principalCondition = []chain.Condition{principalCondFn(principal)}
}
ruleConditions := append(principalCondition, groupedResource.Conditions...)
r := chain.Rule{
Status: status,
Actions: ruleAction,
Resources: chain.Resources{
Inverted: resourceInverted,
Names: groupedResource.Names,
},
Condition: append(ruleConditions, conditions...),
}
engineChain.Rules = append(engineChain.Rules, r)
}
}
}
}
if len(engineChain.Rules) == 0 {
return nil, ErrActionsNotApplicable
}
return &engineChain, nil
}
func getActionTypes(nativeActions []string) ActionTypes {
var res ActionTypes
for _, action := range nativeActions {
if res.Object && res.Container {
break
}
_, isObj := objectNativeOperations[action]
_, isCnr := containerNativeOperations[action]
res.Object = res.Object || isObj || action == Wildcard
res.Container = res.Container || isCnr || action == Wildcard
}
return res
}
func getNativePrincipalsAndConditionFunc(statement Statement, resolver NativeResolver) ([]string, formPrincipalConditionFunc, error) {
var principals []string
var op chain.ConditionType
statementPrincipal, inverted := statement.principal()
if _, ok := statementPrincipal[Wildcard]; ok { // this can be true only if 'inverted' false
principals = []string{Wildcard}
op = chain.CondStringLike
} else {
for principalType, principal := range statementPrincipal {
if principalType != AWSPrincipalType {
return nil, nil, fmt.Errorf("unsupported principal type '%s'", principalType)
}
parsedPrincipal, err := formNativePrincipal(principal, resolver)
if err != nil {
return nil, nil, fmt.Errorf("parse principal: %w", err)
}
principals = append(principals, parsedPrincipal...)
}
op = chain.CondStringEquals
if inverted {
op = chain.CondStringNotEquals
}
}
return principals, func(principal string) chain.Condition {
return chain.Condition{
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 {
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 res, nil
})
}
type GroupedResources struct {
Names []string
Conditions []chain.Condition
}
type ActionTypes struct {
Object bool
Container bool
}
func formNativeResourceNamesAndConditions(names []string, resolver NativeResolver, actionTypes ActionTypes) ([]GroupedResources, error) {
if !actionTypes.Object && !actionTypes.Container {
return nil, ErrActionsNotApplicable
}
res := make([]GroupedResources, 0, len(names))
combined := make(map[string]struct{})
for _, resource := range names {
if err := validateResource(resource); err != nil {
return nil, err
}
if resource == Wildcard {
res = res[:0]
return append(res, formWildcardNativeResource(actionTypes)), nil
}
if !strings.HasPrefix(resource, s3ResourcePrefix) {
continue
}
var bkt, obj string
s3Resource := strings.TrimPrefix(resource, s3ResourcePrefix)
if s3Resource == Wildcard {
res = res[:0]
return append(res, formWildcardNativeResource(actionTypes)), nil
}
if sepIndex := strings.Index(s3Resource, "/"); sepIndex < 0 {
bkt = s3Resource
} else {
bkt = s3Resource[:sepIndex]
obj = s3Resource[sepIndex+1:]
if len(obj) == 0 {
obj = Wildcard
}
}
bktInfo, err := resolver.GetBucketInfo(bkt)
if err != nil {
return nil, err
}
if obj == Wildcard && actionTypes.Object { // this corresponds to arn:aws:s3:::BUCKET/ or arn:aws:s3:::BUCKET/*
combined[fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
continue
}
if obj == "" && actionTypes.Container { // this corresponds to arn:aws:s3:::BUCKET
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),
fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container),
},
Conditions: []chain.Condition{
{
Op: chain.CondStringLike,
Kind: chain.KindResource,
Key: PropertyKeyFilePath,
Value: obj,
},
},
})
}
if len(combined) != 0 {
gr := GroupedResources{Names: make([]string, 0, len(combined))}
for key := range combined {
gr.Names = append(gr.Names, key)
}
res = append(res, gr)
}
return res, nil
}
func formWildcardNativeResource(actionTypes ActionTypes) GroupedResources {
groupedNames := make([]string, 0, 2)
if actionTypes.Object {
groupedNames = append(groupedNames, native.ResourceFormatAllObjects)
}
if actionTypes.Container {
groupedNames = append(groupedNames, native.ResourceFormatAllContainers)
}
return GroupedResources{Names: groupedNames}
}
func formNativePrincipal(principal []string, resolver NativeResolver) ([]string, error) {
res := make([]string, len(principal))
var err error
for i := range principal {
if res[i], err = formPrincipalKey(principal[i], resolver); err != nil {
return nil, err
}
}
return res, nil
}
func formPrincipalKey(principal string, resolver NativeResolver) (string, error) {
account, user, err := parsePrincipalAsIAMUser(principal)
if err != nil {
return "", err
}
key, err := resolver.GetUserKey(account, user)
if err != nil {
return "", fmt.Errorf("get user key: %w", err)
}
return key, nil
}
func formNativeActionNames(names []string) ([]string, error) {
uniqueActions := make(map[string]struct{}, len(names))
for _, action := range names {
if action == Wildcard {
return []string{Wildcard}, nil
}
isIAM, err := validateAction(action)
if err != nil {
return nil, err
}
if isIAM {
continue
}
if action[len(s3ActionPrefix):] == Wildcard {
return []string{Wildcard}, nil
}
nativeActions := actionToNativeOpMap[action]
if len(nativeActions) == 0 {
return nil, ErrActionsNotApplicable
}
for _, nativeAction := range nativeActions {
uniqueActions[nativeAction] = struct{}{}
}
}
res := make([]string, 0, len(uniqueActions))
for key := range uniqueActions {
res = append(res, key)
}
return res, nil
}

270
iam/converter_s3.go Normal file
View file

@ -0,0 +1,270 @@
package iam
import (
"fmt"
"strings"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
)
const condKeyAWSMFAPresent = "aws:MultiFactorAuthPresent"
var actionToS3OpMap = map[string][]string{
s3ActionAbortMultipartUpload: {s3ActionAbortMultipartUpload},
s3ActionCreateBucket: {s3ActionCreateBucket},
s3ActionDeleteBucket: {s3ActionDeleteBucket},
s3ActionDeleteBucketPolicy: {s3ActionDeleteBucketPolicy},
s3ActionDeleteObjectTagging: {s3ActionDeleteObjectTagging},
s3ActionGetBucketLocation: {s3ActionGetBucketLocation},
s3ActionGetBucketNotification: {s3ActionGetBucketNotification},
s3ActionGetBucketPolicy: {s3ActionGetBucketPolicy},
s3ActionGetBucketPolicyStatus: {s3ActionGetBucketPolicyStatus},
s3ActionGetBucketTagging: {s3ActionGetBucketTagging},
s3ActionGetBucketVersioning: {s3ActionGetBucketVersioning},
s3ActionGetObjectAttributes: {s3ActionGetObjectAttributes},
s3ActionGetObjectLegalHold: {s3ActionGetObjectLegalHold},
s3ActionGetObjectRetention: {s3ActionGetObjectRetention},
s3ActionGetObjectTagging: {s3ActionGetObjectTagging},
s3ActionPutBucketNotification: {s3ActionPutBucketNotification},
s3ActionPutBucketPolicy: {s3ActionPutBucketPolicy},
s3ActionPutBucketVersioning: {s3ActionPutBucketVersioning},
s3ActionPutObjectLegalHold: {s3ActionPutObjectLegalHold},
s3ActionPutObjectRetention: {s3ActionPutObjectRetention},
s3ActionPutObjectTagging: {s3ActionPutObjectTagging},
s3ActionPatchObject: {s3ActionPatchObject},
s3ActionListAllMyBuckets: {"s3:ListBuckets"},
s3ActionListBucket: {"s3:HeadBucket", "s3:GetBucketLocation", "s3:ListObjectsV1", "s3:ListObjectsV2"},
s3ActionListBucketVersions: {"s3:ListBucketObjectVersions"},
s3ActionListBucketMultipartUploads: {"s3:ListMultipartUploads"},
s3ActionGetBucketObjectLockConfiguration: {"s3:GetBucketObjectLockConfig"},
s3ActionGetLifecycleConfiguration: {"s3:GetBucketLifecycle"},
s3ActionGetBucketACL: {"s3:GetBucketACL"},
s3ActionGetBucketCORS: {"s3:GetBucketCors"},
s3ActionPutBucketTagging: {"s3:PutBucketTagging", "s3:DeleteBucketTagging"},
s3ActionPutBucketObjectLockConfiguration: {"s3:PutBucketObjectLockConfig"},
s3ActionPutLifecycleConfiguration: {"s3:PutBucketLifecycle", "s3:DeleteBucketLifecycle"},
s3ActionPutBucketACL: {"s3:PutBucketACL"},
s3ActionPutBucketCORS: {"s3:PutBucketCors", "s3:DeleteBucketCors"},
s3ActionListMultipartUploadParts: {"s3:ListParts"},
s3ActionGetObjectACL: {"s3:GetObjectACL"},
s3ActionGetObject: {"s3:GetObject", "s3:HeadObject"},
s3ActionGetObjectVersion: {"s3:GetObject", "s3:HeadObject"},
s3ActionGetObjectVersionACL: {"s3:GetObjectACL"},
s3ActionGetObjectVersionAttributes: {"s3:GetObjectAttributes"},
s3ActionGetObjectVersionTagging: {"s3:GetObjectTagging"},
s3ActionPutObjectACL: {"s3:PutObjectACL"},
s3ActionPutObjectVersionACL: {"s3:PutObjectACL"},
s3ActionPutObjectVersionTagging: {"s3:PutObjectTagging"},
s3ActionPutObject: {
"s3:PutObject", "s3:PostObject", "s3:CopyObject",
"s3:UploadPart", "s3:UploadPartCopy", "s3:CreateMultipartUpload", "s3:CompleteMultipartUpload",
},
s3ActionDeleteObjectVersionTagging: {"s3:DeleteObjectTagging"},
s3ActionDeleteObject: {"s3:DeleteObject", "s3:DeleteMultipleObjects"},
s3ActionDeleteObjectVersion: {"s3:DeleteObject", "s3:DeleteMultipleObjects"},
}
type S3Resolver interface {
GetUserAddress(account, user string) (string, error)
}
func ConvertToS3Chain(p Policy, resolver S3Resolver) (*chain.Chain, error) {
if err := p.Validate(ResourceBasedPolicyType); err != nil {
return nil, err
}
var engineChain chain.Chain
for _, statement := range p.Statement {
status := formStatus(statement)
actions, actionInverted := statement.action()
s3Actions, err := formS3ActionNames(actions)
if err != nil {
return nil, err
}
ruleAction := chain.Actions{Inverted: actionInverted, Names: s3Actions}
if len(ruleAction.Names) == 0 {
continue
}
resources, resourceInverted := statement.resource()
if err := validateS3ResourceNames(resources); err != nil {
return nil, err
}
ruleResource := chain.Resources{Inverted: resourceInverted, Names: resources}
groupedConditions, err := convertToS3ChainCondition(statement.Conditions, resolver)
if err != nil {
return nil, err
}
splitConditions := splitGroupedConditions(groupedConditions)
principals, principalCondFn, err := getS3PrincipalsAndConditionFunc(statement, resolver)
if err != nil {
return nil, err
}
for _, principal := range principals {
for _, conditions := range splitConditions {
var principalCondition []chain.Condition
if principal != Wildcard {
principalCondition = []chain.Condition{principalCondFn(principal)}
}
r := chain.Rule{
Status: status,
Actions: ruleAction,
Resources: ruleResource,
Condition: append(principalCondition, conditions...),
}
engineChain.Rules = append(engineChain.Rules, r)
}
}
}
if len(engineChain.Rules) == 0 {
return nil, ErrActionsNotApplicable
}
return &engineChain, nil
}
func getS3PrincipalsAndConditionFunc(statement Statement, resolver S3Resolver) ([]string, formPrincipalConditionFunc, error) {
var principals []string
var op chain.ConditionType
statementPrincipal, inverted := statement.principal()
if _, ok := statementPrincipal[Wildcard]; ok { // this can be true only if 'inverted' false
principals = []string{Wildcard}
op = chain.CondStringLike
} else {
for principalType, principal := range statementPrincipal {
if principalType != AWSPrincipalType {
return nil, nil, fmt.Errorf("unsupported principal type '%s'", principalType)
}
parsedPrincipal, err := formS3Principal(principal, resolver)
if err != nil {
return nil, nil, fmt.Errorf("parse principal: %w", err)
}
principals = append(principals, parsedPrincipal...)
}
op = chain.CondStringEquals
if inverted {
op = chain.CondStringNotEquals
}
}
return principals, func(principal string) chain.Condition {
return chain.Condition{
Op: op,
Kind: chain.KindRequest,
Key: s3.PropertyKeyOwner,
Value: principal,
}
}, nil
}
func convertToS3ChainCondition(c Conditions, resolver S3Resolver) ([]GroupedConditions, error) {
return convertToChainConditions(c, func(gr GroupedConditions) (GroupedConditions, error) {
for i := range gr.Conditions {
switch {
case gr.Conditions[i].Key == condKeyAWSPrincipalARN:
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
}
}
return gr, nil
})
}
func formS3Principal(principal []string, resolver S3Resolver) ([]string, error) {
res := make([]string, len(principal))
var err error
for i := range principal {
if res[i], err = formPrincipalOwner(principal[i], resolver); err != nil {
return nil, err
}
}
return res, nil
}
func formPrincipalOwner(principal string, resolver S3Resolver) (string, error) {
account, user, err := parsePrincipalAsIAMUser(principal)
if err != nil {
return "", err
}
address, err := resolver.GetUserAddress(account, user)
if err != nil {
return "", fmt.Errorf("get user address: %w", err)
}
return address, nil
}
func validateS3ResourceNames(names []string) error {
for i := range names {
if err := validateResource(names[i]); err != nil {
return err
}
}
return nil
}
func formS3ActionNames(names []string) ([]string, error) {
uniqueActions := make(map[string]struct{}, len(names))
for _, action := range names {
if action == Wildcard {
return []string{Wildcard}, nil
}
isIAM, err := validateAction(action)
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
}

1910
iam/converter_test.go Normal file

File diff suppressed because it is too large Load diff

329
iam/policy.go Normal file
View file

@ -0,0 +1,329 @@
package iam
import (
"encoding/json"
"errors"
"fmt"
)
type (
// Policy grammar https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html
Policy struct {
Version string `json:"Version,omitempty"`
ID string `json:"Id,omitempty"`
Statement Statements `json:"Statement"`
}
Statements []Statement
Statement struct {
ID string `json:"Id,omitempty"`
SID string `json:"Sid,omitempty"`
Principal Principal `json:"Principal,omitempty"`
NotPrincipal Principal `json:"NotPrincipal,omitempty"`
Effect Effect `json:"Effect"`
Action Action `json:"Action,omitempty"`
NotAction Action `json:"NotAction,omitempty"`
Resource Resource `json:"Resource,omitempty"`
NotResource Resource `json:"NotResource,omitempty"`
Conditions Conditions `json:"Condition,omitempty"`
}
Principal map[PrincipalType][]string
Effect string
Action []string
Resource []string
Conditions map[string]Condition
Condition map[string][]string
PolicyType int
PrincipalType string
)
const policyVersion = "2012-10-17"
const (
GeneralPolicyType PolicyType = iota
IdentityBasedPolicyType
ResourceBasedPolicyType
)
const Wildcard = "*"
const (
AllowEffect Effect = "Allow"
DenyEffect Effect = "Deny"
)
func (e Effect) IsValid() bool {
return e == AllowEffect || e == DenyEffect
}
const (
AWSPrincipalType PrincipalType = "AWS"
FederatedPrincipalType PrincipalType = "Federated"
ServicePrincipalType PrincipalType = "Service"
CanonicalUserPrincipalType PrincipalType = "CanonicalUser"
)
func (p PrincipalType) IsValid() bool {
return p == AWSPrincipalType || p == FederatedPrincipalType ||
p == ServicePrincipalType || p == CanonicalUserPrincipalType
}
func (s *Statements) UnmarshalJSON(data []byte) error {
var list []Statement
if err := json.Unmarshal(data, &list); err == nil {
*s = list
return nil
}
var elem Statement
if err := json.Unmarshal(data, &elem); err != nil {
return err
}
*s = []Statement{elem}
return nil
}
func (p *Principal) UnmarshalJSON(data []byte) error {
*p = make(Principal)
var str string
if err := json.Unmarshal(data, &str); err == nil {
if str != Wildcard {
return errors.New("invalid IAM string principal")
}
(*p)[Wildcard] = nil
return nil
}
m := make(map[PrincipalType]any)
if err := json.Unmarshal(data, &m); err != nil {
return err
}
for key, val := range m {
element, ok := val.(string)
if ok {
(*p)[key] = []string{element}
continue
}
list, ok := val.([]any)
if !ok {
return errors.New("invalid principal format")
}
resList := make([]string, len(list))
for i := range list {
val, ok := list[i].(string)
if !ok {
return errors.New("invalid principal format")
}
resList[i] = val
}
(*p)[key] = resList
}
return nil
}
func (a *Action) UnmarshalJSON(data []byte) error {
var list []string
if err := json.Unmarshal(data, &list); err == nil {
*a = list
return nil
}
var elem string
if err := json.Unmarshal(data, &elem); err != nil {
return err
}
*a = []string{elem}
return nil
}
func (r *Resource) UnmarshalJSON(data []byte) error {
var list []string
if err := json.Unmarshal(data, &list); err == nil {
*r = list
return nil
}
var elem string
if err := json.Unmarshal(data, &elem); err != nil {
return err
}
*r = []string{elem}
return nil
}
func (c *Condition) UnmarshalJSON(data []byte) error {
*c = make(Condition)
m := make(map[string]any)
if err := json.Unmarshal(data, &m); err != nil {
return err
}
for key, val := range m {
element, ok := val.(string)
if ok {
(*c)[key] = []string{element}
continue
}
list, ok := val.([]any)
if !ok {
return errors.New("invalid principal format")
}
resList := make([]string, len(list))
for i := range list {
val, ok := list[i].(string)
if !ok {
return errors.New("invalid principal format")
}
resList[i] = val
}
(*c)[key] = resList
}
return nil
}
func (p Policy) Validate(typ PolicyType) error {
if err := p.validate(); err != nil {
return err
}
switch typ {
case IdentityBasedPolicyType:
return p.validateIdentityBased()
case ResourceBasedPolicyType:
return p.validateResourceBased()
default:
return nil
}
}
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)
}
if len(statement.Action) != 0 && len(statement.NotAction) != 0 {
return errors.New("'Actions' and 'NotAction' are mutually exclusive")
}
if statement.Resource != nil && statement.NotResource != nil {
return errors.New("'Resources' and 'NotResource' are mutually exclusive")
}
if len(statement.Resource) == 0 && len(statement.NotResource) == 0 {
return errors.New("one of 'Resources'/'NotResource' must be provided")
}
if len(statement.Principal) != 0 && len(statement.NotPrincipal) != 0 {
return errors.New("'Principal' and 'NotPrincipal' are mutually exclusive")
}
if len(statement.NotPrincipal) != 0 && statement.Effect != DenyEffect {
return errors.New("using 'NotPrincipal' with effect 'Allow' is not supported")
}
principal, _ := statement.principal()
if err := principal.validate(); err != nil {
return err
}
}
return nil
}
func (p Policy) validateIdentityBased() error {
if len(p.ID) != 0 {
return errors.New("'Id' is not allowed for identity-based policy")
}
for _, statement := range p.Statement {
if len(statement.Principal) != 0 || len(statement.NotPrincipal) != 0 {
return errors.New("'Principal' and 'NotPrincipal' are not allowed for identity-based policy")
}
}
return nil
}
func (p Policy) validateResourceBased() error {
for _, statement := range p.Statement {
if len(statement.Principal) == 0 && len(statement.NotPrincipal) == 0 {
return errors.New("'Principal' or 'NotPrincipal' must be provided for resource-based policy")
}
}
return nil
}
func (s Statement) principal() (Principal, bool) {
if len(s.NotPrincipal) != 0 {
return s.NotPrincipal, true
}
return s.Principal, false
}
func (s Statement) action() (Action, bool) {
if len(s.NotAction) != 0 {
return s.NotAction, true
}
return s.Action, false
}
func (s Statement) resource() (Resource, bool) {
if len(s.NotResource) != 0 {
return s.NotResource, true
}
return s.Resource, false
}
func (p Principal) validate() error {
if _, ok := p[Wildcard]; ok && len(p) == 1 {
return nil
}
for key := range p {
if !key.IsValid() {
return fmt.Errorf("unknown principal type: '%s'", key)
}
}
return nil
}

551
iam/policy_test.go Normal file
View file

@ -0,0 +1,551 @@
package iam
import (
"encoding/json"
"testing"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
"github.com/stretchr/testify/require"
)
func TestUnmarshalIAMPolicy(t *testing.T) {
t.Run("simple fields", func(t *testing.T) {
policy := `{
"Version": "2012-10-17",
"Id": "PutObjPolicy",
"Statement": {
"Sid": "DenyObjectsThatAreNotSSEKMS",
"Principal": "*",
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption-aws-kms-key-id": "true"
}
}
}
}`
expected := Policy{
Version: "2012-10-17",
ID: "PutObjPolicy",
Statement: []Statement{{
SID: "DenyObjectsThatAreNotSSEKMS",
Principal: map[PrincipalType][]string{
"*": nil,
},
Effect: DenyEffect,
Action: []string{"s3:PutObject"},
Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
Conditions: map[string]Condition{
"Null": {
"s3:x-amz-server-side-encryption-aws-kms-key-id": {"true"},
},
},
}},
}
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
require.Equal(t, expected, p)
})
t.Run("complex fields", func(t *testing.T) {
policy := `{
"Version": "2012-10-17",
"Statement": [{
"Principal":{
"AWS":[
"arn:aws:iam::111122223333:user/JohnDoe"
]
},
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"
],
"Condition": {
"StringEquals": {
"s3:RequestObjectTag/Department": ["Finance"]
}
}
}]
}`
expected := Policy{
Version: "2012-10-17",
Statement: []Statement{{
Principal: map[PrincipalType][]string{
AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
},
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
Conditions: map[string]Condition{
"StringEquals": {
"s3:RequestObjectTag/Department": {"Finance"},
},
},
}},
}
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
require.Equal(t, expected, p)
raw, err := json.Marshal(expected)
require.NoError(t, err)
require.JSONEq(t, policy, string(raw))
})
t.Run("check principal AWS", func(t *testing.T) {
policy := `{
"Statement": [{
"Principal":{
"AWS":"arn:aws:iam::111122223333:user/JohnDoe"
}
}]
}`
expected := Policy{
Statement: []Statement{{
Principal: map[PrincipalType][]string{
AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
},
}},
}
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
require.Equal(t, expected, p)
})
t.Run("native example", func(t *testing.T) {
policy := `
{
"Version": "xyz",
"Statement": [
{
"Effect": "Allow",
"Action": [
"native:*",
"s3:PutObject",
"s3:GetObject"
],
"Resource": ["*"],
"Principal": {"FrostFS": ["did:frostfs:039e3ee771a223361fe7862f532e9511b57baaae3c3e2622682e99d0e660f7671"]},
"Condition": {"StringEquals": {"native::object::attribute": "iamuser-admin"}}
}
]
}`
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
})
t.Run("condition array", func(t *testing.T) {
policy := `
{
"Statement": [{
"Condition": {"StringLike": {"ec2:InstanceType": ["t1.*", "t2.*", "m3.*"]}}
}]
}`
expected := Policy{
Statement: []Statement{{
Conditions: map[string]Condition{
"StringLike": {"ec2:InstanceType": {"t1.*", "t2.*", "m3.*"}},
},
}},
}
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
require.Equal(t, expected, p)
})
t.Run("'Not*' fields", func(t *testing.T) {
policy := `
{
"Id": "PutObjPolicy",
"Statement": [{
"NotPrincipal": {"AWS":["arn:aws:iam::111122223333:user/Alice"]},
"Effect": "Deny",
"NotAction": "s3:PutObject",
"NotResource": "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"
}]
}`
expected := Policy{
ID: "PutObjPolicy",
Statement: []Statement{{
NotPrincipal: map[PrincipalType][]string{
AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"},
},
Effect: DenyEffect,
NotAction: []string{"s3:PutObject"},
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
}},
}
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
require.Equal(t, expected, p)
})
}
func TestValidatePolicies(t *testing.T) {
for _, tc := range []struct {
name string
policy Policy
typ PolicyType
isValid bool
}{
{
name: "valid permission boundaries",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
}},
},
typ: GeneralPolicyType,
isValid: true,
},
{
name: "general invalid effect",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: "dummy",
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid principal block",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
Principal: map[PrincipalType][]string{Wildcard: nil},
NotPrincipal: map[PrincipalType][]string{Wildcard: nil},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid not principal",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid principal type",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
NotPrincipal: map[PrincipalType][]string{"dummy": {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid action block",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
NotAction: []string{"iam:*"},
Resource: []string{Wildcard},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid resource block",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Resource: []string{Wildcard},
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "invalid resource block",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Resource: []string{},
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "missing resource block",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "missing statement block",
policy: Policy{},
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"},
Resource: []string{Wildcard},
}},
},
typ: IdentityBasedPolicyType,
isValid: true,
},
{
name: "identity based invalid because of id presence",
policy: Policy{
ID: "some-id",
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
}},
},
typ: IdentityBasedPolicyType,
isValid: false,
},
{
name: "identity based invalid because of principal presence",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
Principal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: IdentityBasedPolicyType,
isValid: false,
},
{
name: "identity based invalid because of not principal presence",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: IdentityBasedPolicyType,
isValid: false,
},
{
name: "resource based valid principal",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: DenyEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
Principal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: ResourceBasedPolicyType,
isValid: true,
},
{
name: "resource based valid not principal",
policy: Policy{
ID: "some-id",
Version: policyVersion,
Statement: []Statement{{
Effect: DenyEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: ResourceBasedPolicyType,
isValid: true,
},
{
name: "resource based invalid missing principal",
policy: Policy{
ID: "some-id",
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
}},
},
typ: ResourceBasedPolicyType,
isValid: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
err := tc.policy.Validate(tc.typ)
if tc.isValid {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
}
func TestProcessDenyFirst(t *testing.T) {
identityBasedPolicyStr := `
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": [ "arn:aws:iam::root:user/user-name" ]
},
"Action": ["s3:PutObject" ],
"Resource": "arn:aws:s3:::*"
}
]
}
`
resourceBasedPolicyStr := `
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [ "arn:aws:s3:::test-bucket/*" ]
}
]
}
`
var identityPolicy Policy
err := json.Unmarshal([]byte(identityBasedPolicyStr), &identityPolicy)
require.NoError(t, err)
var resourcePolicy Policy
err = json.Unmarshal([]byte(resourceBasedPolicyStr), &resourcePolicy)
require.NoError(t, err)
mockResolver := newMockUserResolver([]string{"root/user-name"}, []string{"test-bucket"}, "")
identityNativePolicy, err := ConvertToS3Chain(identityPolicy, mockResolver)
require.NoError(t, err)
identityNativePolicy.MatchType = chain.MatchTypeFirstMatch
resourceNativePolicy, err := ConvertToS3Chain(resourcePolicy, mockResolver)
require.NoError(t, err)
s := inmemory.NewInMemory()
target := engine.NamespaceTarget("ns")
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, target, identityNativePolicy)
require.NoError(t, err)
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, target, resourceNativePolicy)
require.NoError(t, err)
resource := testutil.NewResource("arn:aws:s3:::test-bucket/object", nil)
request := testutil.NewRequest("s3:PutObject", resource, map[string]string{s3.PropertyKeyOwner: mockResolver.users["root/user-name"]})
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)
}

333
pkg/chain/chain.go Normal file
View file

@ -0,0 +1,333 @@
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"
)
// ID is the ID of rule chain.
type ID []byte
// MatchType is the match type for chain rules.
type MatchType uint8
const (
// MatchTypeDenyPriority rejects the request if any `Deny` is specified.
MatchTypeDenyPriority MatchType = 0
// MatchTypeFirstMatch returns the first rule action matched to the request.
MatchTypeFirstMatch MatchType = 1
)
//easyjson:json
type Chain struct {
ID ID
Rules []Rule
MatchType MatchType
}
func (c *Chain) Bytes() []byte {
data, err := c.MarshalBinary()
if err != nil {
panic(err)
}
return data
}
func (c *Chain) DecodeBytes(b []byte) error {
return c.UnmarshalBinary(b)
}
type Rule struct {
Status Status
// Actions the operation is applied to.
Actions Actions
// List of the resources the operation is applied to.
Resources Resources
// True iff individual conditions must be combined with the logical OR.
// By default AND is used, so _each_ condition must pass.
Any bool
Condition []Condition
}
type Actions struct {
Inverted bool
Names []string
}
type Resources struct {
Inverted bool
Names []string
}
type Condition struct {
Op ConditionType
Kind ConditionKindType
Key string
Value string
}
type ConditionKindType byte
const (
KindResource ConditionKindType = iota
KindRequest
)
type ConditionType byte
// TODO @fyrchik: reduce the number of conditions.
// Everything from here should be expressable, but we do not need them all.
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
const (
// String condition operators.
CondStringEquals ConditionType = iota
CondStringNotEquals
CondStringEqualsIgnoreCase
CondStringNotEqualsIgnoreCase
CondStringLike
CondStringNotLike
CondStringLessThan
CondStringLessThanEquals
CondStringGreaterThan
CondStringGreaterThanEquals
// Numeric condition operators.
CondNumericEquals
CondNumericNotEquals
CondNumericLessThan
CondNumericLessThanEquals
CondNumericGreaterThan
CondNumericGreaterThanEquals
CondSliceContains
CondIPAddress
CondNotIPAddress
)
var condToStr = []struct {
ct ConditionType
str string
}{
{CondStringEquals, "StringEquals"},
{CondStringNotEquals, "StringNotEquals"},
{CondStringEqualsIgnoreCase, "StringEqualsIgnoreCase"},
{CondStringNotEqualsIgnoreCase, "StringNotEqualsIgnoreCase"},
{CondStringLike, "StringLike"},
{CondStringNotLike, "StringNotLike"},
{CondStringLessThan, "StringLessThan"},
{CondStringLessThanEquals, "StringLessThanEquals"},
{CondStringGreaterThan, "StringGreaterThan"},
{CondStringGreaterThanEquals, "StringGreaterThanEquals"},
{CondNumericEquals, "NumericEquals"},
{CondNumericNotEquals, "NumericNotEquals"},
{CondNumericLessThan, "NumericLessThan"},
{CondNumericLessThanEquals, "NumericLessThanEquals"},
{CondNumericGreaterThan, "NumericGreaterThan"},
{CondNumericGreaterThanEquals, "NumericGreaterThanEquals"},
{CondSliceContains, "SliceContains"},
{CondIPAddress, "IPAddress"},
{CondNotIPAddress, "NotIPAddress"},
}
func (c ConditionType) String() string {
for _, v := range condToStr {
if v.ct == c {
return v.str
}
}
return "unknown condition type"
}
const condSliceContainsDelimiter = "\x00"
// FormCondSliceContainsValue builds value for ObjectResource or ObjectRequest property
// that can be matched by CondSliceContains condition.
func FormCondSliceContainsValue(values []string) string {
return strings.Join(values, condSliceContainsDelimiter)
}
func (c *Condition) Match(req resource.Request) bool {
var val string
switch c.Kind {
case KindResource:
val = req.Resource().Property(c.Key)
case KindRequest:
val = req.Property(c.Key)
default:
panic(fmt.Sprintf("unknown condition type: %d", c.Kind))
}
switch c.Op {
default:
panic(fmt.Sprintf("unimplemented: %d", c.Op))
case CondStringEquals:
return val == c.Value
case CondStringNotEquals:
return val != c.Value
case CondStringEqualsIgnoreCase:
return strings.EqualFold(val, c.Value)
case CondStringNotEqualsIgnoreCase:
return !strings.EqualFold(val, c.Value)
case CondStringLike:
return util.GlobMatch(val, c.Value)
case CondStringNotLike:
return !util.GlobMatch(val, c.Value)
case CondStringLessThan:
return val < c.Value
case CondStringLessThanEquals:
return val <= c.Value
case CondStringGreaterThan:
return val > c.Value
case CondStringGreaterThanEquals:
return val >= c.Value
case CondSliceContains:
return slices.Contains(strings.Split(val, condSliceContainsDelimiter), c.Value)
case CondNumericEquals, CondNumericNotEquals, CondNumericLessThan, CondNumericLessThanEquals, CondNumericGreaterThan,
CondNumericGreaterThanEquals:
return c.matchNumeric(val)
case CondIPAddress, CondNotIPAddress:
return c.matchIP(val)
}
}
func (c *Condition) matchNumeric(val string) bool {
valDecimal, err := fixedn.Fixed8FromString(val)
if err != nil {
return c.Op == CondNumericNotEquals
}
condVal, err := fixedn.Fixed8FromString(c.Value)
if err != nil {
return c.Op == CondNumericNotEquals
}
switch c.Op {
default:
panic(fmt.Sprintf("unimplemented: %d", c.Op))
case CondNumericEquals:
return valDecimal.Equal(condVal)
case CondNumericNotEquals:
return !valDecimal.Equal(condVal)
case CondNumericLessThan:
return valDecimal.LessThan(condVal)
case CondNumericLessThanEquals:
return valDecimal.LessThan(condVal) || valDecimal.Equal(condVal)
case CondNumericGreaterThan:
return valDecimal.GreaterThan(condVal)
case CondNumericGreaterThanEquals:
return valDecimal.GreaterThan(condVal) || valDecimal.Equal(condVal)
}
}
func (c *Condition) matchIP(val string) bool {
ipAddr, err := netip.ParseAddr(val)
if err != nil {
return false
}
prefix, err := netip.ParsePrefix(c.Value)
if err != nil {
return false
}
switch c.Op {
default:
panic(fmt.Sprintf("unimplemented: %d", c.Op))
case CondIPAddress:
return prefix.Contains(ipAddr)
case CondNotIPAddress:
return !prefix.Contains(ipAddr)
}
}
func (r *Rule) Match(req resource.Request) (status Status, matched bool) {
found := len(r.Resources.Names) == 0
for i := range r.Resources.Names {
if util.GlobMatch(req.Resource().Name(), r.Resources.Names[i]) != r.Resources.Inverted {
found = true
break
}
}
if !found {
return NoRuleFound, false
}
for i := range r.Actions.Names {
if util.GlobMatch(req.Operation(), r.Actions.Names[i]) != r.Actions.Inverted {
return r.matchCondition(req)
}
}
return NoRuleFound, false
}
func (r *Rule) matchCondition(obj resource.Request) (status Status, matched bool) {
if r.Any {
return r.matchAny(obj)
}
return r.matchAll(obj)
}
func (r *Rule) matchAny(obj resource.Request) (status Status, matched bool) {
for i := range r.Condition {
if r.Condition[i].Match(obj) {
return r.Status, true
}
}
return NoRuleFound, false
}
func (r *Rule) matchAll(obj resource.Request) (status Status, matched bool) {
for i := range r.Condition {
if !r.Condition[i].Match(obj) {
return NoRuleFound, false
}
}
return r.Status, true
}
func (c *Chain) Match(req resource.Request) (status Status, matched bool) {
switch c.MatchType {
case MatchTypeDenyPriority:
return c.denyPriority(req)
case MatchTypeFirstMatch:
return c.firstMatch(req)
default:
panic(fmt.Sprintf("unknown MatchType %d", c.MatchType))
}
}
func (c *Chain) firstMatch(req resource.Request) (status Status, matched bool) {
for i := range c.Rules {
status, matched := c.Rules[i].Match(req)
if matched {
return status, true
}
}
return NoRuleFound, false
}
func (c *Chain) denyPriority(req resource.Request) (status Status, matched bool) {
var allowFound bool
for i := range c.Rules {
status, matched := c.Rules[i].Match(req)
if !matched {
continue
}
if status != Allow {
return status, true
}
allowFound = true
}
if allowFound {
return Allow, true
}
return NoRuleFound, false
}

BIN
pkg/chain/chain_easyjson.go generated Normal file

Binary file not shown.

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

@ -0,0 +1,13 @@
package chain
// Name represents the place in the request lifecycle where policy is applied.
type Name string
const (
// Ingress represents chains applied when crossing user/storage network boundary.
// It is not applied when talking between nodes.
Ingress Name = "ingress"
// S3 represents chains applied when crossing user/s3 network boundary.
S3 Name = "s3"
)

1028
pkg/chain/chain_test.go Normal file

File diff suppressed because it is too large Load diff

35
pkg/chain/error.go Normal file
View file

@ -0,0 +1,35 @@
package chain
import "fmt"
// Status is the status for policy application.
type Status byte
const (
Allow Status = iota
NoRuleFound
AccessDenied
QuotaLimitReached
last
)
// Valid returns true if the status is valid.
func (s Status) Valid() bool {
return s < last
}
// String implements the fmt.Stringer interface.
func (s Status) String() string {
switch s {
case Allow:
return "Allowed"
case NoRuleFound:
return "NoRuleFound"
case AccessDenied:
return "Access denied"
case QuotaLimitReached:
return "Quota limit reached"
default:
return fmt.Sprintf("Denied with status: %d", s)
}
}

257
pkg/chain/marshal_binary.go Normal file
View file

@ -0,0 +1,257 @@
package chain
import (
"encoding"
"fmt"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/marshal"
)
const (
ChainMarshalVersion uint8 = 0 // increase if breaking change
)
var (
_ encoding.BinaryMarshaler = (*Chain)(nil)
_ encoding.BinaryUnmarshaler = (*Chain)(nil)
)
func (c *Chain) MarshalBinary() ([]byte, error) {
s := marshal.UInt8Size // Marshaller version
s += marshal.UInt8Size // Chain version
s += marshal.SliceSize(c.ID, func(byte) int { return marshal.ByteSize })
s += marshal.SliceSize(c.Rules, ruleSize)
s += marshal.UInt8Size // MatchType
buf := make([]byte, s)
var offset int
var err error
offset, err = marshal.UInt8Marshal(buf, offset, marshal.Version)
if err != nil {
return nil, err
}
offset, err = marshal.UInt8Marshal(buf, offset, ChainMarshalVersion)
if err != nil {
return nil, err
}
offset, err = marshal.SliceMarshal(buf, offset, c.ID, marshal.ByteMarshal)
if err != nil {
return nil, err
}
offset, err = marshal.SliceMarshal(buf, offset, c.Rules, marshalRule)
if err != nil {
return nil, err
}
offset, err = marshal.UInt8Marshal(buf, offset, uint8(c.MatchType))
if err != nil {
return nil, err
}
if err := marshal.VerifyMarshal(buf, offset); err != nil {
return nil, err
}
return buf, nil
}
func (c *Chain) UnmarshalBinary(data []byte) error {
var offset int
marshallerVersion, offset, err := marshal.UInt8Unmarshal(data, offset)
if err != nil {
return err
}
if marshallerVersion != marshal.Version {
return fmt.Errorf("unsupported marshaller version %d", marshallerVersion)
}
chainVersion, offset, err := marshal.UInt8Unmarshal(data, offset)
if err != nil {
return err
}
if chainVersion != ChainMarshalVersion {
return fmt.Errorf("unsupported chain version %d", chainVersion)
}
idBytes, offset, err := marshal.SliceUnmarshal(data, offset, marshal.ByteUnmarshal)
if err != nil {
return err
}
c.ID = ID(idBytes)
c.Rules, offset, err = marshal.SliceUnmarshal(data, offset, unmarshalRule)
if err != nil {
return err
}
matchTypeV, offset, err := marshal.UInt8Unmarshal(data, offset)
if err != nil {
return err
}
c.MatchType = MatchType(matchTypeV)
return marshal.VerifyUnmarshal(data, offset)
}
func ruleSize(r Rule) int {
s := marshal.ByteSize // Status
s += actionsSize(r.Actions)
s += resourcesSize(r.Resources)
s += marshal.BoolSize // Any
s += marshal.SliceSize(r.Condition, conditionSize)
return s
}
func marshalRule(buf []byte, offset int, r Rule) (int, error) {
offset, err := marshal.ByteMarshal(buf, offset, byte(r.Status))
if err != nil {
return 0, err
}
offset, err = marshalActions(buf, offset, r.Actions)
if err != nil {
return 0, err
}
offset, err = marshalResources(buf, offset, r.Resources)
if err != nil {
return 0, err
}
offset, err = marshal.BoolMarshal(buf, offset, r.Any)
if err != nil {
return 0, err
}
return marshal.SliceMarshal(buf, offset, r.Condition, marshalCondition)
}
func unmarshalRule(buf []byte, offset int) (Rule, int, error) {
var r Rule
statusV, offset, err := marshal.ByteUnmarshal(buf, offset)
if err != nil {
return Rule{}, 0, err
}
r.Status = Status(statusV)
r.Actions, offset, err = unmarshalActions(buf, offset)
if err != nil {
return Rule{}, 0, err
}
r.Resources, offset, err = unmarshalResources(buf, offset)
if err != nil {
return Rule{}, 0, err
}
r.Any, offset, err = marshal.BoolUnmarshal(buf, offset)
if err != nil {
return Rule{}, 0, err
}
r.Condition, offset, err = marshal.SliceUnmarshal(buf, offset, unmarshalCondition)
if err != nil {
return Rule{}, 0, err
}
return r, offset, nil
}
func actionsSize(a Actions) int {
return marshal.BoolSize + // Inverted
marshal.SliceSize(a.Names, marshal.StringSize)
}
func marshalActions(buf []byte, offset int, a Actions) (int, error) {
offset, err := marshal.BoolMarshal(buf, offset, a.Inverted)
if err != nil {
return 0, err
}
return marshal.SliceMarshal(buf, offset, a.Names, marshal.StringMarshal)
}
func unmarshalActions(buf []byte, offset int) (Actions, int, error) {
var a Actions
var err error
a.Inverted, offset, err = marshal.BoolUnmarshal(buf, offset)
if err != nil {
return Actions{}, 0, err
}
a.Names, offset, err = marshal.SliceUnmarshal(buf, offset, marshal.StringUnmarshal)
if err != nil {
return Actions{}, 0, err
}
return a, offset, nil
}
func resourcesSize(r Resources) int {
return marshal.BoolSize + // Inverted
marshal.SliceSize(r.Names, marshal.StringSize)
}
func marshalResources(buf []byte, offset int, r Resources) (int, error) {
offset, err := marshal.BoolMarshal(buf, offset, r.Inverted)
if err != nil {
return 0, err
}
return marshal.SliceMarshal(buf, offset, r.Names, marshal.StringMarshal)
}
func unmarshalResources(buf []byte, offset int) (Resources, int, error) {
var r Resources
var err error
r.Inverted, offset, err = marshal.BoolUnmarshal(buf, offset)
if err != nil {
return Resources{}, 0, err
}
r.Names, offset, err = marshal.SliceUnmarshal(buf, offset, marshal.StringUnmarshal)
if err != nil {
return Resources{}, 0, err
}
return r, offset, nil
}
func conditionSize(c Condition) int {
return marshal.ByteSize + // Op
marshal.ByteSize + // Object
marshal.StringSize(c.Key) +
marshal.StringSize(c.Value)
}
func marshalCondition(buf []byte, offset int, c Condition) (int, error) {
offset, err := marshal.ByteMarshal(buf, offset, byte(c.Op))
if err != nil {
return 0, err
}
offset, err = marshal.ByteMarshal(buf, offset, byte(c.Kind))
if err != nil {
return 0, err
}
offset, err = marshal.StringMarshal(buf, offset, c.Key)
if err != nil {
return 0, err
}
return marshal.StringMarshal(buf, offset, c.Value)
}
func unmarshalCondition(buf []byte, offset int) (Condition, int, error) {
var c Condition
opV, offset, err := marshal.ByteUnmarshal(buf, offset)
if err != nil {
return Condition{}, 0, err
}
c.Op = ConditionType(opV)
obV, offset, err := marshal.ByteUnmarshal(buf, offset)
if err != nil {
return Condition{}, 0, err
}
c.Kind = ConditionKindType(obV)
c.Key, offset, err = marshal.StringUnmarshal(buf, offset)
if err != nil {
return Condition{}, 0, err
}
c.Value, offset, err = marshal.StringUnmarshal(buf, offset)
if err != nil {
return Condition{}, 0, err
}
return c, offset, nil
}

View file

@ -0,0 +1,247 @@
package chain
import (
"fmt"
"testing"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
func TestChainMarshalling(t *testing.T) {
t.Parallel()
for _, id := range generateTestIDs() {
for _, rules := range generateTestRules() {
for _, matchType := range generateTestMatchTypes() {
performMarshalTest(t, id, rules, matchType)
}
}
}
}
func TestInvalidChainData(t *testing.T) {
var ch Chain
require.Error(t, ch.UnmarshalBinary(nil))
require.Error(t, ch.UnmarshalBinary([]byte{}))
require.Error(t, ch.UnmarshalBinary([]byte{1, 2, 3}))
require.Error(t, ch.UnmarshalBinary([]byte("\x00\x00:aws:iam::namespace:group/so\x82\x82\x82\x82\x82\x82u\x82")))
}
func performMarshalTest(t *testing.T, id ID, r []Rule, mt MatchType) {
chain := Chain{
ID: id,
Rules: r,
MatchType: mt,
}
data, err := chain.MarshalBinary()
require.NoError(t, err)
var unmarshalledChain Chain
require.NoError(t, unmarshalledChain.UnmarshalBinary(data))
require.Equal(t, chain, unmarshalledChain)
}
func generateTestIDs() []ID {
return []ID{
ID(""),
ID(uuid.New().String()),
ID("*::/"),
ID("avada kedavra"),
ID("arn:aws:iam::namespace:group/some_group"),
ID("$Object:homomorphicHash"),
ID("native:container/ns/9LPLUFZpEmfidG4n44vi2cjXKXSqWT492tCvLJiJ8W1J"),
}
}
func generateTestRules() [][]Rule {
result := [][]Rule{
nil,
{},
{},
}
for _, st := range generateTestStatuses() {
for _, act := range generateTestActions() {
for _, res := range generateTestResources() {
for _, cond := range generateTestConditions() {
result[2] = append(result[2], Rule{
Status: st,
Actions: act,
Resources: res,
Condition: cond,
Any: true,
})
result[2] = append(result[2], Rule{
Status: st,
Actions: act,
Resources: res,
Condition: cond,
})
}
}
}
}
return result
}
func generateTestStatuses() []Status {
return []Status{
Allow,
NoRuleFound,
AccessDenied,
QuotaLimitReached,
}
}
func generateTestActions() []Actions {
return []Actions{
{
Inverted: true,
Names: nil,
},
{
Names: nil,
},
{
Inverted: true,
Names: []string{},
},
{
Names: []string{},
},
{
Inverted: true,
Names: []string{native.MethodPutObject},
},
{
Names: []string{native.MethodPutObject},
},
{
Inverted: true,
Names: []string{native.MethodPutObject, native.MethodDeleteContainer, native.MethodDeleteObject},
},
{
Names: []string{native.MethodPutObject, native.MethodDeleteContainer, native.MethodDeleteObject},
},
}
}
func generateTestResources() []Resources {
return []Resources{
{
Inverted: true,
Names: nil,
},
{
Names: nil,
},
{
Inverted: true,
Names: []string{},
},
{
Names: []string{},
},
{
Inverted: true,
Names: []string{native.ResourceFormatAllObjects},
},
{
Names: []string{native.ResourceFormatAllObjects},
},
{
Inverted: true,
Names: []string{
native.ResourceFormatAllObjects,
fmt.Sprintf(native.ResourceFormatRootContainer, "9LPLUFZpEmfidG4n44vi2cjXKXSqWT492tCvLJiJ8W1J"),
},
},
{
Names: []string{
native.ResourceFormatAllObjects,
fmt.Sprintf(native.ResourceFormatRootContainer, "9LPLUFZpEmfidG4n44vi2cjXKXSqWT492tCvLJiJ8W1J"),
},
},
}
}
func generateTestConditions() [][]Condition {
result := [][]Condition{
nil,
{},
{},
}
for _, ct := range generateTestConditionTypes() {
for _, ot := range generateObjectTypes() {
result[2] = append(result[2], Condition{
Op: ct,
Kind: ot,
Key: "",
Value: "",
})
result[2] = append(result[2], Condition{
Op: ct,
Kind: ot,
Key: "key",
Value: "",
})
result[2] = append(result[2], Condition{
Op: ct,
Kind: ot,
Key: "",
Value: "value",
})
result[2] = append(result[2], Condition{
Op: ct,
Kind: ot,
Key: "key",
Value: "value",
})
}
}
return result
}
func generateTestConditionTypes() []ConditionType {
return []ConditionType{
CondStringEquals,
CondStringNotEquals,
CondStringEqualsIgnoreCase,
CondStringNotEqualsIgnoreCase,
CondStringLike,
CondStringNotLike,
CondStringLessThan,
CondStringLessThanEquals,
CondStringGreaterThan,
CondStringGreaterThanEquals,
CondNumericEquals,
CondNumericNotEquals,
CondNumericLessThan,
CondNumericLessThanEquals,
CondNumericGreaterThan,
CondNumericGreaterThanEquals,
CondSliceContains,
}
}
func generateObjectTypes() []ConditionKindType {
return []ConditionKindType{
KindResource,
KindRequest,
}
}
func generateTestMatchTypes() []MatchType {
return []MatchType{
MatchTypeDenyPriority,
MatchTypeFirstMatch,
}
}

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

@ -0,0 +1,13 @@
//go:build gofuzz
// +build gofuzz
package chain
func DoFuzzChainUnmarshalBinary(data []byte) int {
var ch Chain
err := ch.UnmarshalBinary(data)
if err != nil {
return 0
}
return 1
}

View file

@ -0,0 +1,34 @@
//go:build gofuzz
// +build gofuzz
package chain
import (
"testing"
"github.com/stretchr/testify/require"
)
func FuzzUnmarshal(f *testing.F) {
for _, id := range generateTestIDs() {
for _, rules := range generateTestRules() {
for _, matchType := range generateTestMatchTypes() {
chain := Chain{
ID: id,
Rules: rules,
MatchType: matchType,
}
data, err := chain.MarshalBinary()
require.NoError(f, err)
f.Add(data)
}
}
}
f.Fuzz(func(t *testing.T, data []byte) {
require.NotPanics(t, func() {
DoFuzzChainUnmarshalBinary(data)
})
})
}

145
pkg/chain/marshal_json.go Normal file
View file

@ -0,0 +1,145 @@
package chain
import (
"fmt"
"strconv"
jlexer "github.com/mailru/easyjson/jlexer"
jwriter "github.com/mailru/easyjson/jwriter"
)
// Run `make generate`` if types added or changed
var matchTypeToJSONValue = []struct {
mt MatchType
str string
}{
{MatchTypeDenyPriority, "DenyPriority"},
{MatchTypeFirstMatch, "FirstMatch"},
}
var statusToJSONValue = []struct {
s Status
str string
}{
{Allow, "Allow"},
{NoRuleFound, "NoRuleFound"},
{AccessDenied, "AccessDenied"},
{QuotaLimitReached, "QuotaLimitReached"},
}
var objectTypeToJSONValue = []struct {
t ConditionKindType
str string
}{
{KindRequest, "Request"},
{KindResource, "Resource"},
}
func (mt MatchType) MarshalEasyJSON(w *jwriter.Writer) {
for _, p := range matchTypeToJSONValue {
if p.mt == mt {
w.String(p.str)
return
}
}
w.String(strconv.FormatUint(uint64(mt), 10))
}
func (mt *MatchType) UnmarshalEasyJSON(l *jlexer.Lexer) {
str := l.String()
for _, p := range matchTypeToJSONValue {
if p.str == str {
*mt = p.mt
return
}
}
v, err := strconv.ParseUint(str, 10, 8)
if err != nil {
l.AddError(fmt.Errorf("failed to parse match type: %w", err))
return
}
*mt = MatchType(v)
}
func (st Status) MarshalEasyJSON(w *jwriter.Writer) {
for _, p := range statusToJSONValue {
if p.s == st {
w.String(p.str)
return
}
}
w.String(strconv.FormatUint(uint64(st), 10))
}
func (st *Status) UnmarshalEasyJSON(l *jlexer.Lexer) {
str := l.String()
for _, p := range statusToJSONValue {
if p.str == str {
*st = p.s
return
}
}
v, err := strconv.ParseUint(str, 10, 8)
if err != nil {
l.AddError(fmt.Errorf("failed to parse status: %w", err))
return
}
*st = Status(v)
}
func (ot ConditionKindType) MarshalEasyJSON(w *jwriter.Writer) {
for _, p := range objectTypeToJSONValue {
if p.t == ot {
w.String(p.str)
return
}
}
w.String(strconv.FormatUint(uint64(ot), 10))
}
func (ot *ConditionKindType) UnmarshalEasyJSON(l *jlexer.Lexer) {
str := l.String()
for _, p := range objectTypeToJSONValue {
if p.str == str {
*ot = p.t
return
}
}
v, err := strconv.ParseUint(str, 10, 8)
if err != nil {
l.AddError(fmt.Errorf("failed to parse object type: %w", err))
return
}
*ot = ConditionKindType(v)
}
func (ct ConditionType) MarshalEasyJSON(w *jwriter.Writer) {
for _, p := range condToStr {
if p.ct == ct {
w.String(p.str)
return
}
}
w.String(strconv.FormatUint(uint64(ct), 10))
}
func (ct *ConditionType) UnmarshalEasyJSON(l *jlexer.Lexer) {
str := l.String()
for _, p := range condToStr {
if p.str == str {
*ct = p.ct
return
}
}
v, err := strconv.ParseUint(str, 10, 8)
if err != nil {
l.AddError(fmt.Errorf("failed to parse condition type: %w", err))
return
}
*ct = ConditionType(v)
}

View file

@ -0,0 +1,121 @@
package chain
import (
"fmt"
"os"
"testing"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/stretchr/testify/require"
)
func TestID(t *testing.T) {
key, err := keys.NewPrivateKeyFromWIF("L5eVx6HcHaFpQpvjQ3fy29uKDZ8rQ34bfMVx4XfZMm52EqafpNMg") // s3-gw key
require.NoError(t, err)
chain1 := &Chain{ID: ID(key.PublicKey().GetScriptHash().BytesBE())}
data := chain1.Bytes()
var chain2 Chain
require.NoError(t, chain2.DecodeBytes(data))
require.Equal(t, chain1.ID, chain2.ID)
data, err = chain1.MarshalJSON()
require.NoError(t, err)
require.NoError(t, chain2.UnmarshalJSON(data))
require.Equal(t, chain1.ID, chain2.ID)
}
func TestMatchTypeJson(t *testing.T) {
for _, mt := range []MatchType{MatchTypeDenyPriority, MatchTypeFirstMatch, MatchType(100)} {
var chain Chain
chain.MatchType = mt
data, err := chain.MarshalJSON()
require.NoError(t, err)
if mt == MatchTypeDenyPriority {
require.Equal(t, []byte("{\"ID\":null,\"Rules\":null,\"MatchType\":\"DenyPriority\"}"), data)
} else if mt == MatchTypeFirstMatch {
require.Equal(t, []byte("{\"ID\":null,\"Rules\":null,\"MatchType\":\"FirstMatch\"}"), data)
} else {
require.Equal(t, []byte(fmt.Sprintf("{\"ID\":null,\"Rules\":null,\"MatchType\":\"%d\"}", mt)), data)
}
var parsed Chain
require.NoError(t, parsed.UnmarshalJSON(data))
require.Equal(t, chain, parsed)
require.Error(t, parsed.UnmarshalJSON([]byte("{\"ID\":\"\",\"Rules\":null,\"MatchType\":\"NotValid\"}")))
}
}
func TestJsonEnums(t *testing.T) {
chain := Chain{
ID: []byte("2cca5ae7-cee8-428d-b45f-567fb1d03f01"), // will be encoded to base64
MatchType: MatchTypeFirstMatch,
Rules: []Rule{
{
Status: AccessDenied,
Actions: Actions{
Names: []string{native.MethodDeleteObject, native.MethodGetContainer},
},
Resources: Resources{
Names: []string{native.ResourceFormatAllObjects},
},
Condition: []Condition{
{
Op: CondStringEquals,
Kind: KindRequest,
Key: native.PropertyKeyActorRole,
Value: native.PropertyValueContainerRoleOthers,
},
},
},
{
Status: QuotaLimitReached,
Actions: Actions{
Inverted: true,
Names: []string{native.MethodPutObject},
},
Resources: Resources{
Names: []string{fmt.Sprintf(native.ResourceFormatRootContainerObjects, "9LPLUFZpEmfidG4n44vi2cjXKXSqWT492tCvLJiJ8W1J")},
},
Any: true,
Condition: []Condition{
{
Op: CondStringNotLike,
Kind: KindResource,
Key: native.PropertyKeyObjectType,
Value: "regular",
},
},
},
{
Status: Status(100),
Condition: []Condition{
{
Op: ConditionType(255),
Kind: ConditionKindType(128),
},
},
},
},
}
data, err := chain.MarshalJSON()
require.NoError(t, err)
var parsed Chain
require.NoError(t, parsed.UnmarshalJSON(data))
require.Equal(t, chain, parsed)
expected, err := os.ReadFile("./testdata/test_status_json.json")
require.NoError(t, err)
require.NoError(t, parsed.UnmarshalJSON(expected))
require.Equal(t, chain, parsed)
}

View file

@ -0,0 +1,75 @@
{
"ID": "MmNjYTVhZTctY2VlOC00MjhkLWI0NWYtNTY3ZmIxZDAzZjAx",
"Rules": [
{
"Status": "AccessDenied",
"Actions": {
"Inverted": false,
"Names": [
"DeleteObject",
"GetContainer"
]
},
"Resources": {
"Inverted": false,
"Names": [
"native:object/*"
]
},
"Any": false,
"Condition": [
{
"Op": "StringEquals",
"Kind": "Request",
"Key": "$Actor:role",
"Value": "others"
}
]
},
{
"Status": "QuotaLimitReached",
"Actions": {
"Inverted": true,
"Names": [
"PutObject"
]
},
"Resources": {
"Inverted": false,
"Names": [
"native:object//9LPLUFZpEmfidG4n44vi2cjXKXSqWT492tCvLJiJ8W1J/*"
]
},
"Any": true,
"Condition": [
{
"Op": "StringNotLike",
"Kind": "Resource",
"Key": "$Object:objectType",
"Value": "regular"
}
]
},
{
"Status": "100",
"Actions": {
"Inverted": false,
"Names": null
},
"Resources": {
"Inverted": false,
"Names": null
},
"Any": false,
"Condition": [
{
"Op": "255",
"Kind": "128",
"Key": "",
"Value": ""
}
]
}
],
"MatchType": "FirstMatch"
}

109
pkg/engine/chain_router.go Normal file
View file

@ -0,0 +1,109 @@
package engine
import (
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
)
type defaultChainRouter struct {
morph MorphRuleChainStorageReader
local LocalOverrideStorage
}
func NewDefaultChainRouter(morph MorphRuleChainStorageReader) ChainRouter {
return &defaultChainRouter{
morph: morph,
}
}
func NewDefaultChainRouterWithLocalOverrides(morph MorphRuleChainStorageReader, local LocalOverrideStorage) ChainRouter {
return &defaultChainRouter{
morph: morph,
local: local,
}
}
func (dr *defaultChainRouter) IsAllowed(name chain.Name, rt RequestTarget, r resource.Request) (status chain.Status, ruleFound bool, err error) {
status, ruleFound, err = dr.checkLocal(name, rt, r)
if err != nil {
return chain.NoRuleFound, false, err
} else if ruleFound {
// The local overrides have the highest priority and thus
// morph rules are not considered if a local one is found.
return
}
status, ruleFound, err = dr.checkMorph(name, rt, r)
return
}
func (dr *defaultChainRouter) checkLocal(name chain.Name, rt RequestTarget, r resource.Request) (status chain.Status, ruleFound bool, err error) {
if dr.local == nil {
return
}
var hasAllow bool
for _, target := range rt.Targets() {
status, ruleFound, err = dr.matchLocalOverrides(name, target, r)
if err != nil || ruleFound && status != chain.Allow {
return
}
hasAllow = hasAllow || ruleFound
}
if hasAllow {
return chain.Allow, true, nil
}
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 hasAllow bool
for _, target := range rt.Targets() {
status, ruleFound, err = dr.matchMorphRuleChains(name, target, r)
if err != nil || ruleFound && status != chain.Allow {
return
}
hasAllow = hasAllow || ruleFound
}
if hasAllow {
return chain.Allow, true, nil
}
return chain.NoRuleFound, false, nil
}
func (dr *defaultChainRouter) matchLocalOverrides(name chain.Name, target Target, r resource.Request) (status chain.Status, ruleFound bool, err error) {
localOverrides, err := dr.local.ListOverrides(name, target)
if err != nil {
return
}
status, ruleFound = dr.getStatusFromChains(localOverrides, r)
return
}
func (dr *defaultChainRouter) matchMorphRuleChains(name chain.Name, target Target, r resource.Request) (status chain.Status, ruleFound bool, err error) {
namespaceChains, err := dr.morph.ListMorphRuleChains(name, target)
if err != nil {
return chain.NoRuleFound, false, err
}
status, ruleFound = dr.getStatusFromChains(namespaceChains, r)
return
}
func (dr *defaultChainRouter) getStatusFromChains(chains []*chain.Chain, r resource.Request) (chain.Status, bool) {
var allow bool
for _, c := range chains {
if status, found := c.Match(r); found {
if status != chain.Allow {
return status, true
}
allow = true
}
}
if allow {
return chain.Allow, true
}
return chain.NoRuleFound, false
}

10
pkg/engine/errors.go Normal file
View file

@ -0,0 +1,10 @@
package engine
import "errors"
var (
ErrUnknownTarget = errors.New("unknown target type")
ErrChainNotFound = errors.New("chain not found")
ErrChainNameNotFound = errors.New("chain name not found")
ErrResourceNotFound = errors.New("resource not found")
)

View file

@ -0,0 +1,48 @@
package inmemory
import (
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
)
type inmemory struct {
router engine.ChainRouter
morph engine.MorphRuleChainStorage
local engine.LocalOverrideStorage
}
// NewInMemoryLocalOverrides returns new inmemory instance of chain storage with
// local overrides manager.
func NewInMemoryLocalOverrides() engine.LocalOverrideEngine {
morph := NewInmemoryMorphRuleChainStorage()
local := NewInmemoryLocalStorage()
return &inmemory{
router: engine.NewDefaultChainRouterWithLocalOverrides(morph, local),
morph: morph,
local: local,
}
}
// NewInMemory returns new inmemory instance of chain storage.
func NewInMemory() engine.Engine {
morph := NewInmemoryMorphRuleChainStorage()
return &inmemory{
router: engine.NewDefaultChainRouter(morph),
morph: morph,
}
}
func (im *inmemory) LocalStorage() engine.LocalOverrideStorage {
return im.local
}
func (im *inmemory) MorphRuleChainStorage() engine.MorphRuleChainStorage {
return im.morph
}
func (im *inmemory) IsAllowed(name chain.Name, rt engine.RequestTarget, r resource.Request) (status chain.Status, ruleFound bool, err error) {
return im.router.IsAllowed(name, rt, r)
}

View file

@ -0,0 +1,377 @@
package inmemory
import (
"bytes"
"testing"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
resourcetest "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
func TestAddRootOverrides(t *testing.T) {
s := NewInMemoryLocalOverrides()
target := engine.NamespaceTarget("")
id, err := s.LocalStorage().AddOverride(chain.S3, target, &chain.Chain{
Rules: []chain.Rule{{
Status: chain.Allow,
Actions: chain.Actions{Names: []string{"s3:PutObject"}},
Resources: chain.Resources{Names: []string{"*"}},
}},
})
require.NoError(t, err)
res, err := s.LocalStorage().ListOverrides(chain.S3, target)
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, string(id), string(res[0].ID))
}
func TestInmemory_MultipleTargets(t *testing.T) {
const op = "ape::test::op"
targets := []engine.Target{
engine.NamespaceTarget("ns1"),
engine.ContainerTarget("cnr1"),
engine.GroupTarget("group1"),
engine.UserTarget("user1"),
}
req := resourcetest.NewRequest(op, resourcetest.NewResource("r", nil), nil)
target := engine.NewRequestTargetExtended("ns1", "cnr1", "user1", []string{"group1"})
for _, tt := range targets {
t.Run("morph", func(t *testing.T) {
s := NewInMemoryLocalOverrides()
s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, tt, &chain.Chain{
Rules: []chain.Rule{{
Status: chain.Allow,
Actions: chain.Actions{Names: []string{op}},
}},
})
status, found, err := s.IsAllowed(chain.Ingress, target, req)
require.NoError(t, err)
require.True(t, found)
require.Equal(t, chain.Allow, status)
})
t.Run("override", func(t *testing.T) {
s := NewInMemoryLocalOverrides()
s.LocalStorage().AddOverride(chain.Ingress, tt, &chain.Chain{
Rules: []chain.Rule{{
Status: chain.Allow,
Actions: chain.Actions{Names: []string{op}},
}},
})
status, found, err := s.IsAllowed(chain.Ingress, target, req)
require.NoError(t, err)
require.True(t, found)
require.Equal(t, chain.Allow, status)
})
}
}
func TestInmemory(t *testing.T) {
const (
object = "native::object::abc/xyz"
container = "native::object::abc/*"
namespace = "Tenant1"
namespace2 = "Tenant2"
actor1 = "owner1"
actor2 = "owner2"
)
s := NewInMemoryLocalOverrides()
// Object which was put via S3.
res := resourcetest.NewResource(object, map[string]string{"FromS3": "true"})
// Request initiating from the trusted subnet and actor.
reqGood := resourcetest.NewRequest("native::object::put", res, map[string]string{
"SourceIP": "10.1.1.12",
"Actor": actor1,
})
status, ok, _ := s.IsAllowed(chain.Ingress, engine.NewRequestTargetWithNamespace(namespace), reqGood)
require.Equal(t, chain.NoRuleFound, status)
require.False(t, ok)
s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace), &chain.Chain{
Rules: []chain.Rule{
{ // Restrict to remove ANY object from the namespace.
Status: chain.AccessDenied,
Actions: chain.Actions{Names: []string{"native::object::delete"}},
Resources: chain.Resources{Names: []string{"native::object::*"}},
},
{ // Allow to put object only from the trusted subnet AND trusted actor, deny otherwise.
Status: chain.AccessDenied,
Actions: chain.Actions{Names: []string{"native::object::put"}},
Resources: chain.Resources{Names: []string{"native::object::*"}},
Any: true,
Condition: []chain.Condition{
{
Op: chain.CondStringNotLike,
Kind: chain.KindRequest,
Key: "SourceIP",
Value: "10.1.1.*",
},
{
Op: chain.CondStringNotEquals,
Kind: chain.KindRequest,
Key: "Actor",
Value: actor1,
},
},
},
},
})
_, it, err := s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace)
require.NoError(t, err)
itemStacksEqual(t, it.Values, toStackItems(namespace))
s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace2), &chain.Chain{
Rules: []chain.Rule{
{ // Deny all expect "native::object::get" for all objects expect "native::object::abc/xyz".
Status: chain.AccessDenied,
Actions: chain.Actions{Inverted: true, Names: []string{"native::object::get"}},
Resources: chain.Resources{Inverted: true, Names: []string{object}},
},
},
})
_, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace)
require.NoError(t, err)
itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2))
s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(container), &chain.Chain{
Rules: []chain.Rule{
{ // Allow to actor2 to get objects from the specific container only if they have `Department=HR` attribute.
Status: chain.Allow,
Actions: chain.Actions{Names: []string{"native::object::get"}},
Resources: chain.Resources{Names: []string{"native::object::abc/*"}},
Condition: []chain.Condition{
{
Op: chain.CondStringEquals,
Kind: chain.KindResource,
Key: "Department",
Value: "HR",
},
{
Op: chain.CondStringEquals,
Kind: chain.KindRequest,
Key: "Actor",
Value: actor2,
},
},
},
},
})
_, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace)
require.NoError(t, err)
itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2))
_, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Container)
require.NoError(t, err)
itemStacksEqual(t, it.Values, toStackItems(container))
t.Run("bad subnet, namespace deny", func(t *testing.T) {
// Request initiating from the untrusted subnet.
reqBadIP := resourcetest.NewRequest("native::object::put", res, map[string]string{
"SourceIP": "10.122.1.20",
"Actor": actor1,
})
status, ok, _ := s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqBadIP)
require.Equal(t, chain.AccessDenied, status)
require.True(t, ok)
})
t.Run("bad actor, namespace deny", func(t *testing.T) {
// Request initiating from the untrusted actor.
reqBadActor := resourcetest.NewRequest("native::object::put", res, map[string]string{
"SourceIP": "10.1.1.13",
"Actor": actor2,
})
status, ok, _ := s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqBadActor)
require.Equal(t, chain.AccessDenied, status)
require.True(t, ok)
})
t.Run("bad object, container deny", func(t *testing.T) {
objGood := resourcetest.NewResource("native::object::abc/id1", map[string]string{"Department": "HR"})
objBadAttr := resourcetest.NewResource("native::object::abc/id2", map[string]string{"Department": "Support"})
status, ok, _ := s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), resourcetest.NewRequest("native::object::get", objGood, map[string]string{
"SourceIP": "10.1.1.14",
"Actor": actor2,
}))
require.Equal(t, chain.Allow, status)
require.True(t, ok)
status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), resourcetest.NewRequest("native::object::get", objBadAttr, map[string]string{
"SourceIP": "10.1.1.14",
"Actor": actor2,
}))
require.Equal(t, chain.NoRuleFound, status)
require.False(t, ok)
})
t.Run("bad operation, namespace deny", func(t *testing.T) {
// Request with the forbidden operation.
reqBadOperation := resourcetest.NewRequest("native::object::delete", res, map[string]string{
"SourceIP": "10.1.1.12",
"Actor": actor1,
})
status, ok, _ := s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqBadOperation)
require.Equal(t, chain.AccessDenied, status)
require.True(t, ok)
})
t.Run("inverted rules", func(t *testing.T) {
req := resourcetest.NewRequest("native::object::put", resourcetest.NewResource(object, nil), nil)
status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace2, container), req)
require.Equal(t, chain.NoRuleFound, status)
require.False(t, ok)
req = resourcetest.NewRequest("native::object::put", resourcetest.NewResource("native::object::cba/def", nil), nil)
status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace2, container), req)
require.Equal(t, chain.AccessDenied, status)
require.True(t, ok)
req = resourcetest.NewRequest("native::object::get", resourcetest.NewResource("native::object::cba/def", nil), nil)
status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace2, container), req)
require.Equal(t, chain.NoRuleFound, status)
require.False(t, ok)
})
t.Run("good", func(t *testing.T) {
status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqGood)
require.Equal(t, chain.NoRuleFound, status)
require.False(t, ok)
t.Run("quota on a different container", func(t *testing.T) {
s.LocalStorage().AddOverride(chain.Ingress, engine.ContainerTarget(container), &chain.Chain{
Rules: []chain.Rule{{
Status: chain.QuotaLimitReached,
Actions: chain.Actions{Names: []string{"native::object::put"}},
Resources: chain.Resources{Names: []string{"native::object::cba/*"}},
}},
})
_, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace)
require.NoError(t, err)
itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2))
_, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Container)
require.NoError(t, err)
itemStacksEqual(t, it.Values, toStackItems(container))
status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqGood)
require.Equal(t, chain.NoRuleFound, status)
require.False(t, ok)
})
var quotaRuleChainID chain.ID
t.Run("quota on the request container", func(t *testing.T) {
quotaRuleChainID, _ = s.LocalStorage().AddOverride(chain.Ingress, engine.ContainerTarget(container), &chain.Chain{
Rules: []chain.Rule{{
Status: chain.QuotaLimitReached,
Actions: chain.Actions{Names: []string{"native::object::put"}},
Resources: chain.Resources{Names: []string{"native::object::abc/*"}},
}},
})
_, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace)
require.NoError(t, err)
itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2))
_, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Container)
require.NoError(t, err)
itemStacksEqual(t, it.Values, toStackItems(container))
status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqGood)
require.Equal(t, chain.QuotaLimitReached, status)
require.True(t, ok)
})
t.Run("removed quota on the request container", func(t *testing.T) {
err := s.LocalStorage().RemoveOverride(chain.Ingress, engine.ContainerTarget(container), quotaRuleChainID)
require.NoError(t, err)
_, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace)
require.NoError(t, err)
itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2))
_, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Container)
require.NoError(t, err)
itemStacksEqual(t, it.Values, toStackItems(container))
status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqGood)
require.Equal(t, chain.NoRuleFound, status)
require.False(t, ok)
})
})
t.Run("remove all", func(t *testing.T) {
s := NewInMemoryLocalOverrides()
_, _, err := s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace), &chain.Chain{
Rules: []chain.Rule{
{
Status: chain.AccessDenied,
Actions: chain.Actions{Inverted: true, Names: []string{"native::object::get"}},
Resources: chain.Resources{Inverted: true, Names: []string{object}},
},
},
})
require.NoError(t, err)
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace2), &chain.Chain{
Rules: []chain.Rule{
{
Status: chain.Allow,
Actions: chain.Actions{Inverted: true, Names: []string{"native::object::get"}},
Resources: chain.Resources{Inverted: true, Names: []string{object}},
},
},
})
require.NoError(t, err)
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace2), &chain.Chain{
Rules: []chain.Rule{
{
Status: chain.AccessDenied,
Actions: chain.Actions{Inverted: true, Names: []string{"native::object::get"}},
Resources: chain.Resources{Inverted: true, Names: []string{object}},
},
},
})
require.NoError(t, err)
_, _, err = s.MorphRuleChainStorage().RemoveMorphRuleChainsByTarget(chain.Ingress, engine.NamespaceTarget(namespace2))
require.NoError(t, err)
chains, err := s.MorphRuleChainStorage().ListMorphRuleChains(chain.Ingress, engine.NamespaceTarget(namespace2))
require.NoError(t, err)
require.Equal(t, 0, len(chains))
chains, err = s.MorphRuleChainStorage().ListMorphRuleChains(chain.Ingress, engine.NamespaceTarget(namespace))
require.NoError(t, err)
require.Equal(t, 1, len(chains))
})
}
func itemStacksEqual(t *testing.T, got []stackitem.Item, expected []stackitem.Item) {
next:
for _, exp := range expected {
expBytes, err := exp.TryBytes()
require.NoError(t, err)
for _, v := range got {
vBytes, err := v.TryBytes()
require.NoError(t, err)
if bytes.Equal(vBytes, expBytes) {
continue next
}
}
t.Fatalf("not found %s", exp)
}
}
func toStackItems(names ...string) []stackitem.Item {
var items []stackitem.Item
for _, name := range names {
items = append(items, stackitem.NewByteArray([]byte(name)))
}
return items
}

View file

@ -0,0 +1,180 @@
package inmemory
import (
"bytes"
"fmt"
"math/rand"
"strings"
"sync"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
"git.frostfs.info/TrueCloudLab/policy-engine/util"
)
type targetToChain map[engine.Target][]*chain.Chain
type inmemoryLocalStorage struct {
usedChainID map[string]struct{}
nameToResourceChains map[chain.Name]targetToChain
guard *sync.RWMutex
}
func NewInmemoryLocalStorage() engine.LocalOverrideStorage {
return &inmemoryLocalStorage{
usedChainID: map[string]struct{}{},
nameToResourceChains: make(map[chain.Name]targetToChain),
guard: &sync.RWMutex{},
}
}
func (s *inmemoryLocalStorage) generateChainID(name chain.Name, target engine.Target) chain.ID {
var id chain.ID
for {
suffix := rand.Uint32() % 100
sid := fmt.Sprintf("%s:%s/%d", name, target.Name, suffix)
sid = strings.ReplaceAll(sid, "*", "")
sid = strings.ReplaceAll(sid, "/", ":")
sid = strings.ReplaceAll(sid, "::", ":")
_, ok := s.usedChainID[sid]
if ok {
continue
}
s.usedChainID[sid] = struct{}{}
id = chain.ID(sid)
break
}
return id
}
func (s *inmemoryLocalStorage) AddOverride(name chain.Name, target engine.Target, c *chain.Chain) (chain.ID, error) {
s.guard.Lock()
defer s.guard.Unlock()
if target.Name == "" {
target.Name = "root"
}
// AddOverride assigns generated chain ID if it has not been assigned.
if len(c.ID) == 0 {
c.ID = s.generateChainID(name, target)
}
if s.nameToResourceChains[name] == nil {
s.nameToResourceChains[name] = make(targetToChain)
}
rc := s.nameToResourceChains[name]
for i := range rc[target] {
if bytes.Equal(rc[target][i].ID, c.ID) {
rc[target][i] = c
return c.ID, nil
}
}
rc[target] = append(rc[target], c)
return c.ID, nil
}
func (s *inmemoryLocalStorage) GetOverride(name chain.Name, target engine.Target, chainID chain.ID) (*chain.Chain, error) {
s.guard.RLock()
defer s.guard.RUnlock()
if _, ok := s.nameToResourceChains[name]; !ok {
return nil, engine.ErrChainNameNotFound
}
if target.Name == "" {
target.Name = "root"
}
chains, ok := s.nameToResourceChains[name][target]
if !ok {
return nil, engine.ErrResourceNotFound
}
for _, c := range chains {
if bytes.Equal(c.ID, chainID) {
return c, nil
}
}
return nil, engine.ErrChainNotFound
}
func (s *inmemoryLocalStorage) RemoveOverride(name chain.Name, target engine.Target, chainID chain.ID) error {
s.guard.Lock()
defer s.guard.Unlock()
if _, ok := s.nameToResourceChains[name]; !ok {
return engine.ErrChainNameNotFound
}
if target.Name == "" {
target.Name = "root"
}
chains, ok := s.nameToResourceChains[name][target]
if !ok {
return engine.ErrResourceNotFound
}
for i, c := range chains {
if bytes.Equal(c.ID, chainID) {
s.nameToResourceChains[name][target] = append(chains[:i], chains[i+1:]...)
return nil
}
}
return engine.ErrChainNotFound
}
func (s *inmemoryLocalStorage) RemoveOverridesByTarget(name chain.Name, target engine.Target) error {
s.guard.Lock()
defer s.guard.Unlock()
if _, ok := s.nameToResourceChains[name]; !ok {
return engine.ErrChainNameNotFound
}
if target.Name == "" {
target.Name = "root"
}
_, ok := s.nameToResourceChains[name][target]
if ok {
delete(s.nameToResourceChains[name], target)
return nil
}
return engine.ErrResourceNotFound
}
func (s *inmemoryLocalStorage) ListOverrides(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
s.guard.RLock()
defer s.guard.RUnlock()
rcs, ok := s.nameToResourceChains[name]
if !ok {
return []*chain.Chain{}, nil
}
if target.Name == "" {
target.Name = "root"
}
for t, chains := range rcs {
if t.Type != target.Type {
continue
}
if !util.GlobMatch(target.Name, t.Name) {
continue
}
return chains, nil
}
return []*chain.Chain{}, nil
}
func (s *inmemoryLocalStorage) DropAllOverrides(name chain.Name) error {
s.guard.Lock()
defer s.guard.Unlock()
s.nameToResourceChains[name] = make(targetToChain)
return nil
}
func (s *inmemoryLocalStorage) ListOverrideDefinedTargets(name chain.Name) ([]engine.Target, error) {
s.guard.RLock()
defer s.guard.RUnlock()
ttc := s.nameToResourceChains[name]
var keys []engine.Target
for k := range ttc {
keys = append(keys, k)
}
return keys, nil
}

View file

@ -0,0 +1,268 @@
package inmemory
import (
"testing"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
"github.com/stretchr/testify/require"
)
const (
container = "native:::object/ExYw/*"
chainID = "ingress:ExYw"
nonExistChainId = "ingress:LxGyWyL"
)
var resrc = engine.ContainerTarget(container)
func testInmemLocalStorage() *inmemoryLocalStorage {
return NewInmemoryLocalStorage().(*inmemoryLocalStorage)
}
func TestAddOverride(t *testing.T) {
inmem := testInmemLocalStorage()
inmem.AddOverride(chain.Ingress, resrc, &chain.Chain{
Rules: []chain.Rule{
{
Status: chain.AccessDenied,
Actions: chain.Actions{Names: []string{"native::object::delete"}},
Resources: chain.Resources{Names: []string{"native::object::*"}},
},
},
})
ingressChains, ok := inmem.nameToResourceChains[chain.Ingress]
require.True(t, ok)
resourceChains, ok := ingressChains[resrc]
require.True(t, ok)
require.Len(t, resourceChains, 1)
require.Len(t, resourceChains[0].Rules, 1)
inmem.AddOverride(chain.Ingress, resrc, &chain.Chain{
Rules: []chain.Rule{
{
Status: chain.QuotaLimitReached,
Actions: chain.Actions{Names: []string{"native::object::put"}},
Resources: chain.Resources{Names: []string{"native::object::*"}},
},
{
Status: chain.AccessDenied,
Actions: chain.Actions{Names: []string{"native::object::get"}},
Resources: chain.Resources{Names: []string{"native::object::*"}},
},
},
})
ingressChains, ok = inmem.nameToResourceChains[chain.Ingress]
require.True(t, ok)
resourceChains, ok = ingressChains[resrc]
require.True(t, ok)
require.Len(t, resourceChains, 2)
require.Len(t, resourceChains[1].Rules, 2)
}
func TestRemoveOverride(t *testing.T) {
t.Run("remove from empty storage", func(t *testing.T) {
inmem := testInmemLocalStorage()
err := inmem.RemoveOverride(chain.Ingress, resrc, chain.ID(chainID))
require.ErrorIs(t, err, engine.ErrChainNameNotFound)
})
t.Run("remove not added chain id", func(t *testing.T) {
inmem := testInmemLocalStorage()
inmem.AddOverride(chain.Ingress, resrc, &chain.Chain{
ID: chain.ID(chainID),
Rules: []chain.Rule{
{ // Restrict to remove ANY object from the namespace.
Status: chain.AccessDenied,
Actions: chain.Actions{Names: []string{"native::object::delete"}},
Resources: chain.Resources{Names: []string{"native::object::*"}},
},
},
})
err := inmem.RemoveOverride(chain.Ingress, resrc, chain.ID(nonExistChainId))
require.ErrorIs(t, err, engine.ErrChainNotFound)
})
t.Run("remove existing chain id", func(t *testing.T) {
inmem := testInmemLocalStorage()
inmem.AddOverride(chain.Ingress, resrc, &chain.Chain{
ID: chain.ID(chainID),
Rules: []chain.Rule{
{ // Restrict to remove ANY object from the namespace.
Status: chain.AccessDenied,
Actions: chain.Actions{Names: []string{"native::object::delete"}},
Resources: chain.Resources{Names: []string{"native::object::*"}},
},
},
})
err := inmem.RemoveOverride(chain.Ingress, resrc, chain.ID(chainID))
require.NoError(t, err)
ingressChains, ok := inmem.nameToResourceChains[chain.Ingress]
require.True(t, ok)
require.Len(t, ingressChains, 1)
resourceChains, ok := ingressChains[resrc]
require.True(t, ok)
require.Len(t, resourceChains, 0)
})
t.Run("remove by target", func(t *testing.T) {
inmem := testInmemLocalStorage()
t0 := engine.ContainerTarget("name0")
t1 := engine.ContainerTarget("name1")
inmem.AddOverride(chain.Ingress, t0, &chain.Chain{
ID: chain.ID(chainID),
Rules: []chain.Rule{
{
Status: chain.AccessDenied,
Actions: chain.Actions{Names: []string{"native::object::delete"}},
Resources: chain.Resources{Names: []string{"native::object::*"}},
},
},
})
inmem.AddOverride(chain.Ingress, t0, &chain.Chain{
ID: chain.ID(chainID),
Rules: []chain.Rule{
{
Status: chain.Allow,
Actions: chain.Actions{Names: []string{"native::object::delete"}},
Resources: chain.Resources{Names: []string{"native::object::*"}},
},
},
})
inmem.AddOverride(chain.Ingress, t1, &chain.Chain{
ID: chain.ID(chainID),
Rules: []chain.Rule{
{
Status: chain.Allow,
Actions: chain.Actions{Names: []string{"native::object::delete"}},
Resources: chain.Resources{Names: []string{"native::object::*"}},
},
},
})
err := inmem.RemoveOverridesByTarget(chain.Ingress, t0)
require.NoError(t, err)
ingressChains, ok := inmem.nameToResourceChains[chain.Ingress]
require.True(t, ok)
require.Len(t, ingressChains, 1)
resourceChains, ok := ingressChains[t1]
require.True(t, ok)
require.Len(t, resourceChains, 1)
})
}
func TestGetOverride(t *testing.T) {
addChain := &chain.Chain{
ID: chain.ID(chainID),
Rules: []chain.Rule{
{ // Restrict to remove ANY object from the namespace.
Status: chain.AccessDenied,
Actions: chain.Actions{Names: []string{"native::object::delete"}},
Resources: chain.Resources{Names: []string{"native::object::*"}},
},
},
}
t.Run("get from empty storage", func(t *testing.T) {
inmem := testInmemLocalStorage()
_, err := inmem.GetOverride(chain.Ingress, resrc, chain.ID(chainID))
require.ErrorIs(t, err, engine.ErrChainNameNotFound)
})
t.Run("get not added chain id", func(t *testing.T) {
inmem := testInmemLocalStorage()
inmem.AddOverride(chain.Ingress, resrc, addChain)
const nonExistingChainID = "ingress:LxGyWyL"
_, err := inmem.GetOverride(chain.Ingress, resrc, chain.ID(nonExistingChainID))
require.ErrorIs(t, err, engine.ErrChainNotFound)
})
t.Run("get existing chain id", func(t *testing.T) {
inmem := testInmemLocalStorage()
inmem.AddOverride(chain.Ingress, resrc, addChain)
c, err := inmem.GetOverride(chain.Ingress, resrc, chain.ID(chainID))
require.NoError(t, err)
require.EqualValues(t, *addChain, *c)
})
t.Run("get removed chain id", func(t *testing.T) {
inmem := testInmemLocalStorage()
inmem.AddOverride(chain.Ingress, resrc, addChain)
err := inmem.RemoveOverride(chain.Ingress, resrc, chain.ID(chainID))
require.NoError(t, err)
_, err = inmem.GetOverride(chain.Ingress, resrc, chain.ID(chainID))
require.ErrorIs(t, err, engine.ErrChainNotFound)
})
}
func TestListOverrides(t *testing.T) {
addChain := &chain.Chain{
ID: chain.ID(chainID),
Rules: []chain.Rule{
{ // Restrict to remove ANY object from the namespace.
Status: chain.AccessDenied,
Actions: chain.Actions{Names: []string{"native::object::delete"}},
Resources: chain.Resources{Names: []string{"native::object::*"}},
},
},
}
t.Run("list empty storage", func(t *testing.T) {
inmem := testInmemLocalStorage()
l, _ := inmem.ListOverrides(chain.Ingress, resrc)
require.Len(t, l, 0)
})
t.Run("list with one added resource", func(t *testing.T) {
inmem := testInmemLocalStorage()
inmem.AddOverride(chain.Ingress, resrc, addChain)
l, _ := inmem.ListOverrides(chain.Ingress, resrc)
require.Len(t, l, 1)
targets, err := inmem.ListOverrideDefinedTargets(chain.Ingress)
require.NoError(t, err)
require.Equal(t, []engine.Target{resrc}, targets)
})
t.Run("list after drop", func(t *testing.T) {
inmem := testInmemLocalStorage()
inmem.AddOverride(chain.Ingress, resrc, addChain)
l, _ := inmem.ListOverrides(chain.Ingress, resrc)
require.Len(t, l, 1)
_ = inmem.DropAllOverrides(chain.Ingress)
l, _ = inmem.ListOverrides(chain.Ingress, resrc)
require.Len(t, l, 0)
})
}
func TestGenerateID(t *testing.T) {
inmem := testInmemLocalStorage()
ids := make([]chain.ID, 0, 100)
for i := 0; i < 100; i++ {
ids = append(ids, inmem.generateChainID(chain.Ingress, resrc))
}
require.False(t, hasDuplicates(ids))
}
func hasDuplicates(ids []chain.ID) bool {
seen := make(map[string]bool)
for _, id := range ids {
if seen[string(id)] {
return true
}
seen[string(id)] = true
}
return false
}

View file

@ -0,0 +1,112 @@
package inmemory
import (
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
type inmemoryMorphRuleChainStorage struct {
storage engine.LocalOverrideStorage
}
func NewInmemoryMorphRuleChainStorage() engine.MorphRuleChainStorage {
return &inmemoryMorphRuleChainStorage{
storage: NewInmemoryLocalStorage(),
}
}
func (s *inmemoryMorphRuleChainStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (_ util.Uint256, _ uint32, err error) {
switch target.Type {
case engine.Namespace, engine.Container, engine.User, engine.Group:
_, err = s.storage.AddOverride(name, target, c)
default:
err = engine.ErrUnknownTarget
}
return
}
func (s *inmemoryMorphRuleChainStorage) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (_ util.Uint256, _ uint32, err error) {
switch target.Type {
case engine.Namespace, engine.Container, engine.User, engine.Group:
err = s.storage.RemoveOverride(name, target, chainID)
default:
err = engine.ErrUnknownTarget
}
return
}
func (s *inmemoryMorphRuleChainStorage) RemoveMorphRuleChainsByTarget(name chain.Name, target engine.Target) (_ util.Uint256, _ uint32, err error) {
switch target.Type {
case engine.Namespace, engine.Container, engine.User, engine.Group:
err = s.storage.RemoveOverridesByTarget(name, target)
default:
err = engine.ErrUnknownTarget
}
return
}
func (s *inmemoryMorphRuleChainStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
switch target.Type {
case engine.Namespace, engine.Container, engine.User, engine.Group:
return s.storage.ListOverrides(name, target)
default:
}
return nil, engine.ErrUnknownTarget
}
func (s *inmemoryMorphRuleChainStorage) GetAdmin() (util.Uint160, error) {
panic("not implemented")
}
func (s *inmemoryMorphRuleChainStorage) SetAdmin(_ util.Uint160) (util.Uint256, uint32, error) {
panic("not implemented")
}
func (s *inmemoryMorphRuleChainStorage) ListTargetsIterator(targetType engine.TargetType) (uuid uuid.UUID, it result.Iterator, err error) {
it.Values = make([]stackitem.Item, 0)
switch targetType {
case engine.Namespace:
// Listing targets may look bizarre, because inmemory rule chain storage use inmemory local overrides where
// targets are listed by chain names.
var targets []engine.Target
targets, err = s.storage.ListOverrideDefinedTargets(chain.Ingress)
if err != nil {
return
}
for _, t := range targets {
it.Values = append(it.Values, stackitem.NewByteArray([]byte(t.Name)))
}
targets, err = s.storage.ListOverrideDefinedTargets(chain.S3)
if err != nil {
return
}
for _, t := range targets {
it.Values = append(it.Values, stackitem.NewByteArray([]byte(t.Name)))
}
case engine.Container:
var targets []engine.Target
targets, err = s.storage.ListOverrideDefinedTargets(chain.Ingress)
if err != nil {
return
}
for _, t := range targets {
it.Values = append(it.Values, stackitem.NewByteArray([]byte(t.Name)))
}
targets, err = s.storage.ListOverrideDefinedTargets(chain.S3)
if err != nil {
return
}
for _, t := range targets {
it.Values = append(it.Values, stackitem.NewByteArray([]byte(t.Name)))
}
default:
}
return
}

185
pkg/engine/interface.go Normal file
View file

@ -0,0 +1,185 @@
package engine
import (
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/util"
)
type ChainRouter interface {
// IsAllowed returns status for the operation after all checks.
// The second return value signifies whether a matching rule was found.
IsAllowed(name chain.Name, reqTarget RequestTarget, r resource.Request) (status chain.Status, found bool, err error)
}
// LocalOverrideStorage is the interface to manage local overrides defined
// for a node. Local overrides have a higher priority than chains got from morph storage.
type LocalOverrideStorage interface {
AddOverride(name chain.Name, target Target, c *chain.Chain) (chain.ID, error)
GetOverride(name chain.Name, target Target, chainID chain.ID) (*chain.Chain, error)
RemoveOverride(name chain.Name, target Target, chainID chain.ID) error
RemoveOverridesByTarget(name chain.Name, target Target) error
ListOverrides(name chain.Name, target Target) ([]*chain.Chain, error)
DropAllOverrides(name chain.Name) error
ListOverrideDefinedTargets(name chain.Name) ([]Target, error)
}
type TargetType rune
const (
Namespace TargetType = 'n'
Container TargetType = 'c'
User TargetType = 'u'
Group TargetType = 'g'
)
type Target struct {
Type TargetType
Name string
}
// RequestTarget combines several targets on which the request is performed.
type RequestTarget struct {
Namespace *Target
Container *Target
User *Target
Groups []Target
}
func NewRequestTargetWithNamespace(namespace string) RequestTarget {
nt := NamespaceTarget(namespace)
return RequestTarget{
Namespace: &nt,
}
}
func NewRequestTargetWithContainer(container string) RequestTarget {
ct := ContainerTarget(container)
return RequestTarget{
Container: &ct,
}
}
func NewRequestTarget(namespace, container string) RequestTarget {
nt := NamespaceTarget(namespace)
ct := ContainerTarget(container)
return RequestTarget{
Namespace: &nt,
Container: &ct,
}
}
func NewRequestTargetExtended(namespace, container, user string, groups []string) RequestTarget {
nt := NamespaceTarget(namespace)
ct := ContainerTarget(container)
u := UserTarget(user)
rt := RequestTarget{
Namespace: &nt,
Container: &ct,
User: &u,
}
if len(groups) != 0 {
rt.Groups = make([]Target, len(groups))
for i := range groups {
rt.Groups[i] = GroupTarget(groups[i])
}
}
return rt
}
func (rt *RequestTarget) Targets() (targets []Target) {
if rt.Namespace != nil {
targets = append(targets, *rt.Namespace)
}
if rt.Container != nil {
targets = append(targets, *rt.Container)
}
if rt.User != nil {
targets = append(targets, *rt.User)
}
if len(rt.Groups) != 0 {
targets = append(targets, rt.Groups...)
}
return
}
func NamespaceTarget(namespace string) Target {
return Target{
Type: Namespace,
Name: namespace,
}
}
func ContainerTarget(container string) Target {
return Target{
Type: Container,
Name: container,
}
}
func UserTarget(user string) Target {
return Target{
Type: User,
Name: user,
}
}
func GroupTarget(group string) Target {
return Target{
Type: Group,
Name: group,
}
}
// MorphRuleChainStorageReader is the interface that provides read-only methods to receive
// data like chains, target or admin from a chain storage.
type MorphRuleChainStorageReader interface {
// ListMorphRuleChains just lists deserialized chains.
ListMorphRuleChains(name chain.Name, target Target) ([]*chain.Chain, error)
GetAdmin() (util.Uint160, error)
// ListTargetsIterator provides an iterator to list targets for which rules are defined.
ListTargetsIterator(targetType TargetType) (uuid.UUID, result.Iterator, error)
}
// MorphRuleChainStorage is the interface to read and manage data within a chain storage.
// Basically, this implies that the storage manages rules stored in policy contract.
type MorphRuleChainStorage interface {
MorphRuleChainStorageReader
// AddMorphRuleChain adds a chain rule to the policy contract and returns transaction hash, VUB and error.
AddMorphRuleChain(name chain.Name, target Target, c *chain.Chain) (util.Uint256, uint32, error)
// RemoveMorphRuleChain removes a chain rule to the policy contract and returns transaction hash, VUB and error.
RemoveMorphRuleChain(name chain.Name, target Target, chainID chain.ID) (util.Uint256, uint32, error)
// RemoveMorphRuleChainsByTarget removes all chains by target and returns transaction hash, VUB and error.
RemoveMorphRuleChainsByTarget(name chain.Name, target Target) (util.Uint256, uint32, error)
SetAdmin(addr util.Uint160) (util.Uint256, uint32, error)
}
// Engine is the interface that provides methods to check request permissions checking
// chain rules from morph client - this implies using the policy contract.
type Engine interface {
ChainRouter
MorphRuleChainStorage() MorphRuleChainStorage
}
// LocalOverrideEngine is extended Engine that also provides methods to manage a local
// chain rule storage. Local overrides must have the highest priority during request checking.
type LocalOverrideEngine interface {
Engine
LocalStorage() LocalOverrideStorage
}

267
pkg/marshal/marshal.go Normal file
View file

@ -0,0 +1,267 @@
package marshal
import (
"encoding/binary"
"fmt"
)
const (
Version byte = 0 // increase if breaking change
ByteSize int = 1
UInt8Size int = ByteSize
BoolSize int = ByteSize
nilSlice int64 = -1
nilSliceSize int = 1
byteTrue uint8 = 1
byteFalse uint8 = 0
// maxSliceLen taken from https://github.com/neo-project/neo/blob/38218bbee5bbe8b33cd8f9453465a19381c9a547/src/Neo/IO/Helper.cs#L77
maxSliceLen = 0x1000000
)
type MarshallerError struct {
errMsg string
offset int
}
func (e *MarshallerError) Error() string {
if e == nil {
return ""
}
if e.offset < 0 {
return e.errMsg
}
return fmt.Sprintf("%s (offset: %d)", e.errMsg, e.offset)
}
func errBufTooSmall(t string, marshal bool, offset int) error {
action := "unmarshal"
if marshal {
action = "marshal"
}
return &MarshallerError{
errMsg: fmt.Sprintf("not enough bytes left to %s value of type '%s'", action, t),
offset: offset,
}
}
func VerifyMarshal(buf []byte, lastOffset int) error {
if len(buf) != lastOffset {
return &MarshallerError{
errMsg: "actual data size differs from expected",
offset: -1,
}
}
return nil
}
func VerifyUnmarshal(buf []byte, lastOffset int) error {
if len(buf) != lastOffset {
return &MarshallerError{
errMsg: "unmarshalled bytes left",
}
}
return nil
}
func SliceSize[T any](slice []T, sizeOf func(T) int) int {
if slice == nil {
return nilSliceSize
}
s := Int64Size(int64(len(slice)))
for _, v := range slice {
s += sizeOf(v)
}
return s
}
func SliceMarshal[T any](buf []byte, offset int, slice []T, marshalT func([]byte, int, T) (int, error)) (int, error) {
if slice == nil {
return Int64Marshal(buf, offset, nilSlice)
}
if len(slice) > maxSliceLen {
return 0, &MarshallerError{
errMsg: fmt.Sprintf("slice size if too big: '%d'", len(slice)),
offset: offset,
}
}
offset, err := Int64Marshal(buf, offset, int64(len(slice)))
if err != nil {
return 0, err
}
for _, v := range slice {
offset, err = marshalT(buf, offset, v)
if err != nil {
return 0, err
}
}
return offset, nil
}
func SliceUnmarshal[T any](buf []byte, offset int, unmarshalT func(buf []byte, offset int) (T, int, error)) ([]T, int, error) {
size, offset, err := Int64Unmarshal(buf, offset)
if err != nil {
return nil, 0, err
}
if size == nilSlice {
return nil, offset, nil
}
if size > maxSliceLen {
return nil, 0, &MarshallerError{
errMsg: fmt.Sprintf("slice size if too big: '%d'", size),
offset: offset,
}
}
if size < 0 {
return nil, 0, &MarshallerError{
errMsg: fmt.Sprintf("invalid slice size: '%d'", size),
offset: offset,
}
}
result := make([]T, size)
for idx := 0; idx < len(result); idx++ {
result[idx], offset, err = unmarshalT(buf, offset)
if err != nil {
return nil, 0, err
}
}
return result, offset, nil
}
func Int64Size(v int64) int {
// https://cs.opensource.google/go/go/+/master:src/encoding/binary/varint.go;l=92;drc=dac9b9ddbd5160c5f4552410f5f8281bd5eed38c
// and
// https://cs.opensource.google/go/go/+/master:src/encoding/binary/varint.go;l=41;drc=dac9b9ddbd5160c5f4552410f5f8281bd5eed38c
ux := uint64(v) << 1
if v < 0 {
ux = ^ux
}
s := 0
for ux >= 0x80 {
s++
ux >>= 7
}
return s + 1
}
func Int64Marshal(buf []byte, offset int, v int64) (int, error) {
if len(buf)-offset < Int64Size(v) {
return 0, errBufTooSmall("int64", true, offset)
}
return offset + binary.PutVarint(buf[offset:], v), nil
}
func Int64Unmarshal(buf []byte, offset int) (int64, int, error) {
v, read := binary.Varint(buf[offset:])
if read == 0 {
return 0, 0, errBufTooSmall("int64", false, offset)
}
if read < 0 {
return 0, 0, &MarshallerError{
errMsg: "int64 unmarshal overflow",
offset: offset,
}
}
return v, offset + read, nil
}
func StringSize(s string) int {
return Int64Size(int64(len(s))) + len(s)
}
func StringMarshal(buf []byte, offset int, s string) (int, error) {
if len(s) > maxSliceLen {
return 0, &MarshallerError{
errMsg: fmt.Sprintf("string is too long: '%d'", len(s)),
offset: offset,
}
}
if len(buf)-offset < Int64Size(int64(len(s)))+len(s) {
return 0, errBufTooSmall("string", true, offset)
}
offset, err := Int64Marshal(buf, offset, int64(len(s)))
if err != nil {
return 0, err
}
if s == "" {
return offset, nil
}
return offset + copy(buf[offset:], s), nil
}
func StringUnmarshal(buf []byte, offset int) (string, int, error) {
size, offset, err := Int64Unmarshal(buf, offset)
if err != nil {
return "", 0, err
}
if size == 0 {
return "", offset, nil
}
if size > maxSliceLen {
return "", 0, &MarshallerError{
errMsg: fmt.Sprintf("string is too long: '%d'", size),
offset: offset,
}
}
if size < 0 {
return "", 0, &MarshallerError{
errMsg: fmt.Sprintf("invalid string size: '%d'", size),
offset: offset,
}
}
if len(buf)-offset < int(size) {
return "", 0, errBufTooSmall("string", false, offset)
}
return string(buf[offset : offset+int(size)]), offset + int(size), nil
}
func UInt8Marshal(buf []byte, offset int, value uint8) (int, error) {
if len(buf)-offset < 1 {
return 0, errBufTooSmall("uint8", true, offset)
}
buf[offset] = value
return offset + 1, nil
}
func UInt8Unmarshal(buf []byte, offset int) (uint8, int, error) {
if len(buf)-offset < 1 {
return 0, 0, errBufTooSmall("uint8", false, offset)
}
return buf[offset], offset + 1, nil
}
func ByteMarshal(buf []byte, offset int, value byte) (int, error) {
return UInt8Marshal(buf, offset, value)
}
func ByteUnmarshal(buf []byte, offset int) (byte, int, error) {
return UInt8Unmarshal(buf, offset)
}
func BoolMarshal(buf []byte, offset int, value bool) (int, error) {
if value {
return UInt8Marshal(buf, offset, byteTrue)
}
return UInt8Marshal(buf, offset, byteFalse)
}
func BoolUnmarshal(buf []byte, offset int) (bool, int, error) {
v, offset, err := UInt8Unmarshal(buf, offset)
if err != nil {
return false, 0, err
}
if v == byteTrue {
return true, offset, nil
}
if v == byteFalse {
return false, offset, nil
}
return false, 0, &MarshallerError{
errMsg: fmt.Sprintf("invalid marshalled value for bool: %d", v),
offset: offset - BoolSize,
}
}

313
pkg/marshal/marshal_test.go Normal file
View file

@ -0,0 +1,313 @@
package marshal
import (
"encoding/binary"
"math"
"testing"
"github.com/stretchr/testify/require"
)
func TestMarshalling(t *testing.T) {
t.Parallel()
t.Run("slice", func(t *testing.T) {
t.Parallel()
t.Run("nil slice", func(t *testing.T) {
t.Parallel()
var int64s []int64
expectedSize := SliceSize(int64s, Int64Size)
require.Equal(t, 1, expectedSize)
buf := make([]byte, expectedSize)
offset, err := SliceMarshal(buf, 0, int64s, Int64Marshal)
require.NoError(t, err)
require.NoError(t, VerifyMarshal(buf, offset))
result, offset, err := SliceUnmarshal(buf, 0, Int64Unmarshal)
require.NoError(t, err)
require.NoError(t, VerifyUnmarshal(buf, offset))
require.Nil(t, result)
})
t.Run("empty slice", func(t *testing.T) {
t.Parallel()
int64s := make([]int64, 0)
expectedSize := SliceSize(int64s, Int64Size)
require.Equal(t, 1, expectedSize)
buf := make([]byte, expectedSize)
offset, err := SliceMarshal(buf, 0, int64s, Int64Marshal)
require.NoError(t, err)
require.NoError(t, VerifyMarshal(buf, offset))
result, offset, err := SliceUnmarshal(buf, 0, Int64Unmarshal)
require.NoError(t, err)
require.NoError(t, VerifyUnmarshal(buf, offset))
require.NotNil(t, result)
require.Len(t, result, 0)
})
t.Run("non empty slice", func(t *testing.T) {
t.Parallel()
int64s := make([]int64, 100)
for i := range int64s {
int64s[i] = int64(i)
}
expectedSize := SliceSize(int64s, Int64Size)
buf := make([]byte, expectedSize)
offset, err := SliceMarshal(buf, 0, int64s, Int64Marshal)
require.NoError(t, err)
require.NoError(t, VerifyMarshal(buf, offset))
result, offset, err := SliceUnmarshal(buf, 0, Int64Unmarshal)
require.NoError(t, err)
require.NoError(t, VerifyUnmarshal(buf, offset))
require.Equal(t, int64s, result)
})
t.Run("corrupted slice size", func(t *testing.T) {
t.Parallel()
int64s := make([]int64, 100)
for i := range int64s {
int64s[i] = int64(i)
}
expectedSize := SliceSize(int64s, Int64Size)
buf := make([]byte, expectedSize)
offset, err := SliceMarshal(buf, 0, int64s, Int64Marshal)
require.NoError(t, err)
require.NoError(t, VerifyMarshal(buf, offset))
for i := 0; i < binary.MaxVarintLen64; i++ {
buf[i] = 129
}
_, _, err = SliceUnmarshal(buf, 0, Int64Unmarshal)
var mErr *MarshallerError
require.ErrorAs(t, err, &mErr)
for i := 0; i < binary.MaxVarintLen64; i++ {
buf[i] = 127
}
_, _, err = SliceUnmarshal(buf, 0, Int64Unmarshal)
require.ErrorAs(t, err, &mErr)
})
t.Run("corrupted slice item", func(t *testing.T) {
t.Parallel()
int64s := make([]int64, 100)
for i := range int64s {
int64s[i] = int64(i)
}
expectedSize := SliceSize(int64s, Int64Size)
buf := make([]byte, expectedSize)
offset, err := SliceMarshal(buf, 0, int64s, Int64Marshal)
require.NoError(t, err)
require.NoError(t, VerifyMarshal(buf, offset))
for i := 2; i < binary.MaxVarintLen64+2; i++ {
buf[i] = 129
}
_, _, err = SliceUnmarshal(buf, 0, Int64Unmarshal)
var mErr *MarshallerError
require.ErrorAs(t, err, &mErr)
})
t.Run("small buffer", func(t *testing.T) {
t.Parallel()
int64s := make([]int64, 100)
for i := range int64s {
int64s[i] = int64(i)
}
buf := make([]byte, 1)
_, err := SliceMarshal(buf, 0, int64s, Int64Marshal)
var mErr *MarshallerError
require.ErrorAs(t, err, &mErr)
buf = make([]byte, 10)
_, err = SliceMarshal(buf, 0, int64s, Int64Marshal)
require.ErrorAs(t, err, &mErr)
})
})
t.Run("int64", func(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
require.Equal(t, 1, Int64Size(0))
require.Equal(t, binary.MaxVarintLen64, Int64Size(math.MaxInt64))
require.Equal(t, binary.MaxVarintLen64, Int64Size(math.MinInt64))
for _, v := range []int64{0, math.MinInt64, math.MaxInt64} {
size := Int64Size(v)
buf := make([]byte, size)
offset, err := Int64Marshal(buf, 0, v)
require.NoError(t, err)
require.NoError(t, VerifyMarshal(buf, offset))
uv, offset, err := Int64Unmarshal(buf, 0)
require.NoError(t, err)
require.NoError(t, VerifyUnmarshal(buf, offset))
require.Equal(t, v, uv)
}
})
t.Run("invalid buffer", func(t *testing.T) {
t.Parallel()
var mErr *MarshallerError
_, err := Int64Marshal([]byte{}, 0, 100500)
require.ErrorAs(t, err, &mErr)
_, _, err = Int64Unmarshal(nil, 0)
require.ErrorAs(t, err, &mErr)
})
t.Run("overflow", func(t *testing.T) {
t.Parallel()
var mErr *MarshallerError
var v int64 = math.MaxInt64
buf := make([]byte, Int64Size(v))
_, err := Int64Marshal(buf, 0, v)
require.NoError(t, err)
buf[9] = 2
_, _, err = Int64Unmarshal(buf, 0)
require.ErrorAs(t, err, &mErr)
})
})
t.Run("string", func(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
for _, v := range []string{
"", "arn:aws:iam::namespace:group/some_group", "$Object:homomorphicHash",
"native:container/ns/9LPLUFZpEmfidG4n44vi2cjXKXSqWT492tCvLJiJ8W1J",
} {
size := StringSize(v)
buf := make([]byte, size)
offset, err := StringMarshal(buf, 0, v)
require.NoError(t, err)
require.NoError(t, VerifyMarshal(buf, offset))
uv, offset, err := StringUnmarshal(buf, 0)
require.NoError(t, err)
require.NoError(t, VerifyUnmarshal(buf, offset))
require.Equal(t, v, uv)
}
})
t.Run("invalid buffer", func(t *testing.T) {
t.Parallel()
str := "avada kedavra"
var mErr *MarshallerError
_, err := StringMarshal(nil, 0, str)
require.ErrorAs(t, err, &mErr)
_, _, err = StringUnmarshal(nil, 0)
require.ErrorAs(t, err, &mErr)
buf := make([]byte, StringSize(str))
offset, err := StringMarshal(buf, 0, str)
require.NoError(t, err)
require.NoError(t, VerifyMarshal(buf, offset))
buf = buf[:len(buf)-1]
_, _, err = StringUnmarshal(buf, 0)
require.ErrorAs(t, err, &mErr)
})
})
t.Run("uint8, byte", func(t *testing.T) {
t.Parallel()
for _, v := range []byte{0, 8, 16, 32, 64, 128, 255} {
buf := make([]byte, ByteSize)
offset, err := ByteMarshal(buf, 0, v)
require.NoError(t, err)
require.NoError(t, VerifyMarshal(buf, offset))
ub, offset, err := ByteUnmarshal(buf, 0)
require.NoError(t, err)
require.NoError(t, VerifyUnmarshal(buf, offset))
require.Equal(t, v, ub)
buf = make([]byte, UInt8Size)
offset, err = UInt8Marshal(buf, 0, v)
require.NoError(t, err)
require.NoError(t, VerifyMarshal(buf, offset))
uu, offset, err := UInt8Unmarshal(buf, 0)
require.NoError(t, err)
require.NoError(t, VerifyUnmarshal(buf, offset))
require.Equal(t, v, uu)
}
})
t.Run("bool", func(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
for _, v := range []bool{false, true} {
buf := make([]byte, BoolSize)
offset, err := BoolMarshal(buf, 0, v)
require.NoError(t, err)
require.NoError(t, VerifyMarshal(buf, offset))
ub, offset, err := BoolUnmarshal(buf, 0)
require.NoError(t, err)
require.NoError(t, VerifyUnmarshal(buf, offset))
require.Equal(t, v, ub)
}
})
t.Run("invalid value", func(t *testing.T) {
t.Parallel()
buf := make([]byte, BoolSize)
offset, err := BoolMarshal(buf, 0, true)
require.NoError(t, err)
require.NoError(t, VerifyMarshal(buf, offset))
buf[0] = 2
_, _, err = BoolUnmarshal(buf, 0)
var mErr *MarshallerError
require.ErrorAs(t, err, &mErr)
})
t.Run("invalid buffer", func(t *testing.T) {
t.Parallel()
var mErr *MarshallerError
_, err := BoolMarshal(nil, 0, true)
require.ErrorAs(t, err, &mErr)
buf := append(make([]byte, BoolSize), 100)
offset, err := BoolMarshal(buf, 0, true)
require.NoError(t, err)
require.ErrorAs(t, VerifyMarshal(buf, offset), &mErr)
v, offset, err := BoolUnmarshal(buf, 0)
require.NoError(t, err)
require.True(t, v)
require.ErrorAs(t, VerifyUnmarshal(buf, offset), &mErr)
_, _, err = BoolUnmarshal(nil, 0)
require.ErrorAs(t, err, &mErr)
})
})
}

View file

@ -0,0 +1,248 @@
package policy
import (
"errors"
"fmt"
"math/big"
"strings"
"git.frostfs.info/TrueCloudLab/frostfs-contract/commonclient"
"git.frostfs.info/TrueCloudLab/frostfs-contract/policy"
client "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/policy"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
neoinvoker "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet"
)
var (
ErrEmptyChainID = errors.New("chain id is not set")
ErrEngineTargetTypeUnsupported = errors.New("this target type is not supported yet")
)
// ContractStorage is the interface to manage chain rules within Policy contract.
type ContractStorage struct {
hash util.Uint160
actor ContractStorageActor
contractInterface *client.Contract
}
var _ engine.MorphRuleChainStorage = (*ContractStorage)(nil)
// ContractStorageReader is the interface to read data from Policy contract.
type ContractStorageReader struct {
hash util.Uint160
invoker ContractStorageInvoker
contractReaderInterface *client.ContractReader
}
type ContractStorageActor interface {
client.Actor
GetRPCInvoker() neoinvoker.RPCInvoke
}
var _ engine.MorphRuleChainStorageReader = (*ContractStorageReader)(nil)
func NewContractStorage(actor ContractStorageActor, contract util.Uint160) *ContractStorage {
return &ContractStorage{
hash: contract,
actor: actor,
contractInterface: client.New(actor, contract),
}
}
type contractStorageActorImpl struct {
client.Actor
rpcActor actor.RPCActor
}
var _ ContractStorageActor = &contractStorageActorImpl{}
func (c *contractStorageActorImpl) GetRPCInvoker() neoinvoker.RPCInvoke {
return c.rpcActor
}
// NewContractStorageWithSimpleActor constructs core actor from `rpcActor`.
//
// Note: NewContractStorageWithSimpleActor is appropriate only for call-only-once cases (for example, in CLIs). Otherwise, it is unsafe,
// because core actor may use invalidated `rpcActor` if some connection errors occurred.
func NewContractStorageWithSimpleActor(rpcActor actor.RPCActor, acc *wallet.Account, contract util.Uint160) (*ContractStorage, error) {
act, err := actor.NewSimple(rpcActor, acc)
if err != nil {
return nil, fmt.Errorf("failed to create simple actor: %w", err)
}
return NewContractStorage(&contractStorageActorImpl{Actor: act, rpcActor: rpcActor}, contract), nil
}
func (s *ContractStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (txHash util.Uint256, vub uint32, err error) {
if len(c.ID) == 0 {
err = ErrEmptyChainID
return
}
var kind policy.Kind
kind, err = policyKind(target.Type)
if err != nil {
return
}
fullName := prefixedChainName(name, c.ID)
txHash, vub, err = s.contractInterface.AddChain(big.NewInt(int64(kind)), target.Name, fullName, c.Bytes())
return
}
func (s *ContractStorage) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (txHash util.Uint256, vub uint32, err error) {
if len(chainID) == 0 {
err = ErrEmptyChainID
return
}
var kind policy.Kind
kind, err = policyKind(target.Type)
if err != nil {
return
}
fullName := prefixedChainName(name, chainID)
txHash, vub, err = s.contractInterface.RemoveChain(big.NewInt(int64(kind)), target.Name, fullName)
return
}
func (s *ContractStorage) RemoveMorphRuleChainsByTarget(name chain.Name, target engine.Target) (txHash util.Uint256, vub uint32, err error) {
var kind policy.Kind
kind, err = policyKind(target.Type)
if err != nil {
return
}
fullName := prefixedChainName(name, nil)
txHash, vub, err = s.contractInterface.RemoveChainsByPrefix(big.NewInt(int64(kind)), target.Name, fullName)
return
}
func listChains(name chain.Name, target engine.Target, rpcInvoker neoinvoker.RPCInvoke, hash util.Uint160) ([]*chain.Chain, error) {
kind, err := policyKind(target.Type)
if err != nil {
return nil, err
}
const (
method = "iteratorChainsByPrefix"
batchSize = neoinvoker.DefaultIteratorResultItems
)
inv := neoinvoker.New(rpcInvoker, nil)
params := []any{
big.NewInt(int64(kind)), target.Name, []byte(name),
}
items, err := commonclient.ReadIteratorItems(inv, batchSize, hash, method, params...)
if err != nil {
return nil, fmt.Errorf("read items error: %w", err)
}
var chains []*chain.Chain
for _, item := range items {
serialized, err := bytesFromStackItem(item)
if err != nil {
return nil, err
}
c := new(chain.Chain)
if err := c.DecodeBytes(serialized); err != nil {
return nil, err
}
chains = append(chains, c)
}
return chains, nil
}
func (s *ContractStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
return listChains(name, target, s.actor.GetRPCInvoker(), s.hash)
}
func (s *ContractStorage) ListTargetsIterator(targetType engine.TargetType) (uuid.UUID, result.Iterator, error) {
kind, err := policyKind(targetType)
if err != nil {
return uuid.UUID{}, result.Iterator{}, err
}
return s.contractInterface.ListTargets(big.NewInt(int64(kind)))
}
func (s *ContractStorage) GetAdmin() (util.Uint160, error) {
return s.contractInterface.GetAdmin()
}
func (s *ContractStorage) SetAdmin(addr util.Uint160) (util.Uint256, uint32, error) {
return s.contractInterface.SetAdmin(addr)
}
type ContractStorageInvoker interface {
client.Invoker
GetRPCInvoker() neoinvoker.RPCInvoke
}
func NewContractStorageReader(inv ContractStorageInvoker, contract util.Uint160) *ContractStorageReader {
return &ContractStorageReader{
hash: contract,
invoker: inv,
contractReaderInterface: client.NewReader(inv, contract),
}
}
func (s *ContractStorageReader) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
return listChains(name, target, s.invoker.GetRPCInvoker(), s.hash)
}
func (s *ContractStorageReader) GetAdmin() (util.Uint160, error) {
return s.contractReaderInterface.GetAdmin()
}
func (s *ContractStorageReader) ListTargetsIterator(targetType engine.TargetType) (uuid.UUID, result.Iterator, error) {
kind, err := policyKind(targetType)
if err != nil {
return uuid.UUID{}, result.Iterator{}, err
}
return s.contractReaderInterface.ListTargets(big.NewInt(int64(kind)))
}
func bytesFromStackItem(param stackitem.Item) ([]byte, error) {
switch param.Type() {
case stackitem.BufferT, stackitem.ByteArrayT, stackitem.IntegerT:
return param.TryBytes()
case stackitem.AnyT:
if param.Value() == nil {
return nil, nil
}
fallthrough
default:
return nil, fmt.Errorf("chain/client: %s is not a byte array type", param.Type())
}
}
func prefixedChainName(name chain.Name, chainID chain.ID) []byte {
return []byte(strings.ToLower(fmt.Sprintf("%s:%s", name, chainID)))
}
func policyKind(typ engine.TargetType) (policy.Kind, error) {
switch typ {
case engine.Namespace:
return policy.Namespace, nil
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
}
}

19
pkg/resource/resource.go Normal file
View file

@ -0,0 +1,19 @@
package resource
// Request represents generic named resource (bucket, container etc.).
// Name is resource depenent but should be globally unique for any given
// type of resource.
type Request interface {
// Name is the operation name, such as Object.Put. Must not include wildcards.
Operation() string
// Property returns request properties, such as IP address of the origin.
Property(string) string
// Resource returns resource the operation is applied to.
Resource() Resource
}
// Resource represents the resource operation is applied to.
type Resource interface {
Name() string
Property(string) string
}

View file

@ -0,0 +1,53 @@
package testutil
import (
resourcepkg "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
)
type Resource struct {
name string
properties map[string]string
}
func (r *Resource) Name() string {
return r.name
}
func (r *Resource) Property(name string) string {
return r.properties[name]
}
func NewResource(name string, properties map[string]string) *Resource {
if properties == nil {
properties = make(map[string]string)
}
return &Resource{name: name, properties: properties}
}
type Request struct {
operation string
properties map[string]string
resource *Resource
}
var _ resourcepkg.Request = (*Request)(nil)
func (r *Request) Operation() string {
return r.operation
}
func (r *Request) Resource() resourcepkg.Resource {
return r.resource
}
func (r *Request) Property(name string) string {
return r.properties[name]
}
func NewRequest(op string, r *Resource, properties map[string]string) *Request {
return &Request{
operation: op,
properties: properties,
resource: r,
}
}

11
schema/common/consts.go Normal file
View file

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

60
schema/native/consts.go Normal file
View file

@ -0,0 +1,60 @@
package native
const (
MethodGetObject = "GetObject"
MethodPutObject = "PutObject"
MethodHeadObject = "HeadObject"
MethodDeleteObject = "DeleteObject"
MethodSearchObject = "SearchObject"
MethodRangeObject = "RangeObject"
MethodHashObject = "HashObject"
MethodPatchObject = "PatchObject"
MethodPutContainer = "PutContainer"
MethodDeleteContainer = "DeleteContainer"
MethodGetContainer = "GetContainer"
MethodListContainers = "ListContainers"
MethodSetContainerEACL = "SetContainerEACL"
MethodGetContainerEACL = "GetContainerEACL"
ObjectPrefix = "native:object"
ContainerPrefix = "native:container"
ResourceFormatNamespaceObjects = "native:object/%s/*"
ResourceFormatNamespaceContainerObjects = "native:object/%s/%s/*"
ResourceFormatNamespaceContainerObject = "native:object/%s/%s/%s"
ResourceFormatRootObjects = "native:object//*"
ResourceFormatRootContainerObjects = "native:object//%s/*"
ResourceFormatRootContainerObject = "native:object//%s/%s"
ResourceFormatAllObjects = "native:object/*"
ResourceFormatNamespaceContainer = "native:container/%s/%s"
ResourceFormatNamespaceContainers = "native:container/%s/*"
ResourceFormatRootContainer = "native:container//%s"
ResourceFormatRootContainers = "native:container//*"
ResourceFormatAllContainers = "native:container/*"
PropertyKeyActorPublicKey = "$Actor:publicKey"
PropertyKeyActorRole = "$Actor:role"
PropertyKeyObjectVersion = "$Object:version"
PropertyKeyObjectID = "$Object:objectID"
PropertyKeyObjectContainerID = "$Object:containerID"
PropertyKeyObjectOwnerID = "$Object:ownerID"
PropertyKeyObjectCreationEpoch = "$Object:creationEpoch"
PropertyKeyObjectPayloadLength = "$Object:payloadLength"
PropertyKeyObjectPayloadHash = "$Object:payloadHash"
PropertyKeyObjectType = "$Object:objectType"
PropertyKeyObjectHomomorphicHash = "$Object:homomorphicHash"
PropertyKeyContainerOwnerID = "$Container:ownerID"
PropertyValueContainerRoleOwner = "owner"
PropertyValueContainerRoleIR = "ir"
PropertyValueContainerRoleContainer = "container"
PropertyValueContainerRoleOthers = "others"
)

View file

@ -0,0 +1,45 @@
package util
import (
"strings"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
)
var nativePatterns = []string{
native.ResourceFormatNamespaceObjects, native.ResourceFormatNamespaceContainerObjects,
native.ResourceFormatNamespaceContainerObject, native.ResourceFormatRootObjects,
native.ResourceFormatRootContainerObjects, native.ResourceFormatRootContainerObject,
native.ResourceFormatAllObjects, native.ResourceFormatNamespaceContainer,
native.ResourceFormatNamespaceContainers, native.ResourceFormatRootContainer,
native.ResourceFormatRootContainers, native.ResourceFormatAllContainers,
}
func match(resource, pattern string) bool {
rTokens := strings.Split(resource, "/")
pToken := strings.Split(pattern, "/")
if len(rTokens) != len(pToken) {
return false
}
for i := range rTokens {
if pToken[i] == "%s" {
continue
}
if pToken[i] != rTokens[i] {
return false
}
}
return true
}
func IsNativeResourceNameValid(resource string) bool {
for _, pattern := range nativePatterns {
if match(resource, pattern) {
return true
}
}
return false
}

View file

@ -0,0 +1,97 @@
package util
import (
"testing"
"github.com/stretchr/testify/require"
)
var tests = []struct {
name string
expected bool
resource string
}{
{
name: "ResourceFormatNamespaceObjects",
expected: true,
resource: "native:object/RootNamespace/*",
},
{
name: "ResourceFormatNamespaceContainerObjects",
expected: true,
resource: "native:object/RootNamespace/BzQw5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB3R/*",
},
{
name: "ResourceFormatNamespaceContainerObject",
expected: true,
resource: "native:object/RootNamespace/BzQw5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB3R/AeZa5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB4E",
},
{
name: "ResourceFormatRootObjects",
expected: true,
resource: "native:object//*",
},
{
name: "ResourceFormatRootContainerObjects",
expected: true,
resource: "native:object//BzQw5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB3R/*",
},
{
name: "ResourceFormatRootContainerObject",
expected: true,
resource: "native:object//BzQw5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB3R/AeZa5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB4E",
},
{
name: "ResourceFormatAllObjects",
expected: true,
resource: "native:object/*",
},
{
name: "ResourceFormatNamespaceContainer",
expected: true,
resource: "native:container/RootNamespace/BzQw5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB3R",
},
{
name: "ResourceFormatNamespaceContainers",
expected: true,
resource: "native:container/RootNamespace/*",
},
{
name: "ResourceFormatRootContainers",
expected: true,
resource: "native:container//*",
},
{
name: "ResourceFormatAllContainers",
expected: true,
resource: "native:container/*",
},
{
name: "Invalid resource 1",
expected: false,
resource: "native:::container/*",
},
{
name: "Invalid resource 2",
expected: false,
resource: "native:container/RootNamespace/w5HH3feoxFDD5tCTtoRzB3R/Bz726qzgLfxEE7wgtoRzB3R/RootNamespace",
},
}
func TestIsNativeResourceNameValid(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.expected, IsNativeResourceNameValid(test.resource))
})
}
}
func BenchmarkIsNativeResourceNameValid(b *testing.B) {
for _, test := range tests {
b.Run(test.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = IsNativeResourceNameValid(test.resource)
}
})
}
}

24
schema/s3/consts.go Normal file
View file

@ -0,0 +1,24 @@
package s3
const (
PropertyKeyOwner = "Owner"
PropertyKeyDelimiter = "s3:delimiter"
PropertyKeyPrefix = "s3:prefix"
PropertyKeyVersionID = "s3:VersionId"
PropertyKeyMaxKeys = "s3:max-keys"
PropertyKeyFormatResourceTag = "aws:ResourceTag/%s"
PropertyKeyFormatRequestTag = "aws:RequestTag/%s"
PropertyKeyAccessBoxAttrMFA = "AccessBox-Attribute/IAM-MFA"
PropertyKeyFormatAccessBoxAttr = "AccessBox-Attribute/%s"
ResourceFormatS3All = "arn:aws:s3:::*"
ResourceFormatS3Bucket = "arn:aws:s3:::%s"
ResourceFormatS3BucketObjects = "arn:aws:s3:::%s/*"
ResourceFormatS3BucketObject = "arn:aws:s3:::%s/%s"
ResourceFormatIAMNamespaceUser = "arn:aws:iam::%s:user/%s"
ResourceFormatIAMNamespaceGroup = "arn:aws:iam::%s:group/%s"
)

22
util/glob.go Normal file
View file

@ -0,0 +1,22 @@
package util
import (
"strings"
"unicode/utf8"
)
// Matches s against the pattern.
// ? in pattern correspond to any symbol.
// * in pattern correspond to any sequence of symbols.
// Currently only '*' in the suffix is supported.
func GlobMatch(s, pattern string) bool {
index := strings.IndexByte(pattern, '*')
switch index {
default:
panic("unimplemented")
case -1:
return pattern == s
case utf8.RuneCountInString(pattern) - 1:
return strings.HasPrefix(s, pattern[:len(pattern)-1])
}
}